}
/**
+ * Create a new sequence object from a characters array using default values
+ * of 1 and -1 for start and end. The array used to create the sequence is
+ * copied and is not stored internally.
+ *
+ * @param name
+ * sequence name
+ * @param sequence
+ * list of residues
+ */
+ public Sequence(String name, char[] sequence)
+ {
+ this(name, Arrays.copyOf(sequence, sequence.length), 1, -1);
+ }
+
+ /**
* Creates a new Sequence object with new AlignmentAnnotations but inherits
* any existing dataset sequence reference. If non exists, everything is
* copied.
import jalview.ws.params.ParamDatastoreI;
import jalview.ws.params.WsParamSetI;
import jalview.ws.seqfetcher.DbSourceProxy;
-import jalview.ws.slivkaws.SlivkaWSDiscoverer;
+import jalview.ws2.client.api.WebServiceDiscovererI;
+import jalview.ws2.client.slivka.SlivkaWSDiscoverer;
+import jalview.ws2.gui.WebServicesMenuManager;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
{
public static int frameCount;
+
public static final int DEFAULT_WIDTH = 700;
public static final int DEFAULT_HEIGHT = 500;
private int id;
private DataSourceType protocol ;
+
/**
* Creates a new AlignFrame object with specific width and height.
*
public AlignFrame(AlignmentI al, HiddenColumns hiddenColumns, int width,
int height, String sequenceSetId, String viewId)
{
+
id = (++frameCount);
+
setSize(width, height);
if (al.getDataset() == null)
* initalise the alignframe from the underlying viewport data and the
* configurations
*/
+
void init()
{
boolean newPanel = (alignPanel == null);
alignPanel = new AlignmentPanel(this, viewport);
}
addAlignmentPanel(alignPanel, newPanel);
+
// setBackground(Color.white); // BH 2019
if (!Jalview.isHeadlessMode())
}
addFocusListener(new FocusAdapter()
{
+
@Override
public void focusGained(FocusEvent e)
{
* @param format
* format of file
*/
+
@Deprecated
public void setFileName(String file, FileFormatI format)
{
*
* @param file
*/
+
public void setFileObject(File file)
{
this.fileObject = file;
* Add a KeyListener with handlers for various KeyPressed and KeyReleased
* events
*/
+
void addKeyListener()
{
addKeyListener(new KeyAdapter()
{
+
@Override
public void keyPressed(KeyEvent evt)
{
{
buildWebServicesMenu();
}
+
/* Set up intrinsic listeners for dynamically generated GUI bits. */
private void addServiceListeners()
{
* Configure menu items that vary according to whether the alignment is
* nucleotide or protein
*/
+
public void setGUINucleotide()
{
AlignmentI al = getViewport().getAlignment();
* operation that affects the data in the current view (selection changed,
* etc) to update the menus to reflect the new state.
*/
+
@Override
public void setMenusForViewport()
{
* @param av
* AlignViewport
*/
+
public void setMenusFromViewport(AlignViewport av)
{
padGapsMenuitem.setSelected(av.isPadGaps());
*
* @param b
*/
+
public void setGroovyEnabled(boolean b)
{
runGroovy.setEnabled(b);
*
* @see jalview.gui.IProgressIndicator#setProgressBar(java.lang.String, long)
*/
+
@Override
public void setProgressBar(String message, long id)
{
*
* @return true if any progress bars are still active
*/
+
@Override
public boolean operationInProgress()
{
* will cause the status bar to be hidden, with possibly undesirable flicker
* of the screen layout.
*/
+
@Override
public void setStatus(String text)
{
/*
* Added so Castor Mapping file can obtain Jalview Version
*/
+
public String getVersion()
{
return Cache.getProperty("VERSION");
final FeatureSettings nfs = newframe.featureSettings;
SwingUtilities.invokeLater(new Runnable()
{
+
@Override
public void run()
{
}
this.closeMenuItem_actionPerformed(true);
}
+
}
@Override
* Saves the alignment to a file with a name chosen by the user, if necessary
* warning if a file would be overwritten
*/
+
@Override
public void saveAs_actionPerformed()
{
*
* @return true if last call to saveAlignment(file, format) was successful.
*/
+
public boolean isSaveAlignmentSuccessful()
{
* @param file
* @param format
*/
+
public void saveAlignment(String file, FileFormatI format)
{
lastSaveSuccessful = true;
AlignExportSettingsI options = new AlignExportSettingsAdapter(false);
Runnable cancelAction = new Runnable()
{
+
@Override
public void run()
{
};
Runnable outputAction = new Runnable()
{
+
@Override
public void run()
{
Console.trace("ALIGNFRAME about to write to temp file "
+ backupfiles.getTempFilePath());
}
-
out.print(output);
Console.trace("ALIGNFRAME about to close file");
out.close();
*
* @param fileFormatName
*/
+
@Override
protected void outputText_actionPerformed(String fileFormatName)
{
AlignExportSettingsI options = new AlignExportSettingsAdapter(false);
Runnable outputAction = new Runnable()
{
+
@Override
public void run()
{
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void htmlMenuItem_actionPerformed(ActionEvent e)
{
}
// ??
+
public void createImageMap(File file, String image)
{
alignPanel.makePNGImageMap(file, image);
*
* @param f
*/
+
@Override
public void createPNG(File f)
{
*
* @param f
*/
+
@Override
public void createEPS(File f)
{
*
* @param f
*/
+
@Override
public void createSVG(File f)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void printMenuItem_actionPerformed(ActionEvent e)
{
chooser.setToolTipText(tooltip);
chooser.setResponseHandler(0, new Runnable()
{
+
@Override
public void run()
{
*
* @param closeAllTabs
*/
+
@Override
public void closeMenuItem_actionPerformed(boolean closeAllTabs)
{
*
* @param panelToClose
*/
+
public void closeView(AlignmentPanel panelToClose)
{
int index = tabbedPane.getSelectedIndex();
/**
* DOCUMENT ME!
*/
+
void updateEditMenuBar()
{
*
* @return alignment objects for all views
*/
+
AlignmentI[] getViewAlignments()
{
if (alignPanels != null)
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void undoMenuItem_actionPerformed(ActionEvent e)
{
// viewport.getColumnSelection()
// .getHiddenColumns().size() > 0);
originalSource.notifyAlignment();
+
}
}
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void redoMenuItem_actionPerformed(ActionEvent e)
{
// viewport.getColumnSelection()
// .getHiddenColumns().size() > 0);
originalSource.notifyAlignment();
+
}
}
* @param up
* or down (if !up)
*/
+
public void moveSelectedSequences(boolean up)
{
SequenceGroup sg = viewport.getSelectionGroup();
{
return;
}
-
// TODO: JAL-3733 - add an event to the undo buffer for this !
-
viewport.getAlignment().moveSelectedSequencesByOne(sg,
viewport.getHiddenRepSequences(), up);
alignPanel.paintAlignment(true, false);
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void copy_actionPerformed()
{
* @throws InterruptedException
* @throws IOException
*/
+
@Override
protected void pasteNew_actionPerformed(ActionEvent e)
throws IOException, InterruptedException
* @throws InterruptedException
* @throws IOException
*/
+
@Override
protected void pasteThis_actionPerformed(ActionEvent e)
throws IOException, InterruptedException
System.out.println("Exception whilst pasting: " + ex);
// could be anything being pasted in here
}
-
}
@Override
AlignFrame af = new AlignFrame(alignment, DEFAULT_WIDTH,
DEFAULT_HEIGHT);
String newtitle = new String("Flanking alignment");
-
Desktop d = Desktop.getInstance();
if (d.jalviewClipboard != null && d.jalviewClipboard[2] != null)
{
/**
* Action Cut (delete and copy) the selected region
*/
+
@Override
protected void cut_actionPerformed()
{
/**
* Performs menu option to Delete the currently selected region
*/
+
@Override
protected void delete_actionPerformed()
{
Runnable okAction = new Runnable()
{
+
@Override
public void run()
{
viewport.getAlignment().deleteGroup(sg);
viewport.notifyAlignment();
+
if (viewport.getAlignment().getHeight() < 1)
{
try
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void deleteGroups_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void selectAllSequenceMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void deselectAllSequenceMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void invertSequenceMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void remove2LeftMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void remove2RightMenuItem_actionPerformed(ActionEvent e)
{
}
viewport.notifyAlignment();
+
}
}
* @param e
* DOCUMENT ME!
*/
+
@Override
public void removeGappedColumnMenuItem_actionPerformed(ActionEvent e)
{
ranges.setStartRes(seq.findIndex(startRes) - 1);
viewport.notifyAlignment();
+
}
/**
* @param e
* DOCUMENT ME!
*/
+
@Override
public void removeAllGapsMenuItem_actionPerformed(ActionEvent e)
{
viewport.getAlignment()));
viewport.getRanges().setStartRes(seq.findIndex(startRes) - 1);
-
viewport.notifyAlignment();
}
* @param e
* DOCUMENT ME!
*/
+
@Override
public void padGapsMenuitem_actionPerformed(ActionEvent e)
{
viewport.setPadGaps(padGapsMenuitem.isSelected());
viewport.notifyAlignment();
+
}
/**
*
* @param e
*/
+
@Override
public void findMenuItem_actionPerformed(ActionEvent e)
{
/**
* Create a new view of the current alignment.
*/
+
@Override
public void newView_actionPerformed(ActionEvent e)
{
* if true then duplicate all annnotation, groups and settings
* @return new alignment panel, already displayed.
*/
+
public AlignmentPanel newView(String viewTitle, boolean copyAnnotation)
{
/*
* @param viewTitle
* @return
*/
+
protected String getNewViewName(String viewTitle)
{
int index = Desktop.getViewCount(viewport.getSequenceSetId());
* @param comps
* @return
*/
+
protected List<String> getExistingViewNames(List<Component> comps)
{
List<String> existingNames = new ArrayList<>();
/**
* Explode tabbed views into separate windows.
*/
+
@Override
public void expandViews_actionPerformed(ActionEvent e)
{
/**
* Gather views in separate windows back into a tabbed presentation.
*/
+
@Override
public void gatherViews_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void font_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void seqLimit_actionPerformed(ActionEvent e)
{
*
* @see jalview.jbgui.GAlignFrame#followHighlight_actionPerformed()
*/
+
@Override
protected void followHighlight_actionPerformed()
{
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void colourTextMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void wrapMenuItem_actionPerformed(ActionEvent e)
{
* @param toggleSeqs
* @param toggleCols
*/
+
protected void toggleHiddenRegions(boolean toggleSeqs, boolean toggleCols)
{
* jalview.jbgui.GAlignFrame#hideAllButSelection_actionPerformed(java.awt.
* event.ActionEvent)
*/
+
@Override
public void hideAllButSelection_actionPerformed(ActionEvent e)
{
* jalview.jbgui.GAlignFrame#hideAllSelection_actionPerformed(java.awt.event
* .ActionEvent)
*/
+
@Override
public void hideAllSelection_actionPerformed(ActionEvent e)
{
* jalview.jbgui.GAlignFrame#showAllhidden_actionPerformed(java.awt.event.
* ActionEvent)
*/
+
@Override
public void showAllhidden_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void scaleAbove_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void scaleLeft_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void scaleRight_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void viewBoxesMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void viewTextMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void renderGapsMenuItem_actionPerformed(ActionEvent e)
{
* @param evt
* DOCUMENT ME!
*/
+
@Override
public void showSeqFeatures_actionPerformed(ActionEvent evt)
{
*
* @param e
*/
+
@Override
public void annotationPanelMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void overviewMenuItem_actionPerformed(ActionEvent e)
{
}
JInternalFrame frame = new JInternalFrame();
+
// BH 2019.07.26 we allow for an embedded
// undecorated overview with defined size
frame.setName(Platform.getAppID("overview"));
dim = null; // hidden, not embedded
}
OverviewPanel overview = new OverviewPanel(alignPanel, dim);
+
frame.setContentPane(overview);
if (dim == null)
{
frame.addInternalFrameListener(
new javax.swing.event.InternalFrameAdapter()
{
+
@Override
public void internalFrameClosed(
javax.swing.event.InternalFrameEvent evt)
* CovariationColourScheme(viewport.getAlignment().getAlignmentAnnotation
* ()[0])); }
*/
+
@Override
public void annotationColour_actionPerformed()
{
*
* @param selected
*/
+
@Override
public void applyToAllGroups_actionPerformed(boolean selected)
{
* @param name
* the name (not the menu item label!) of the colour scheme
*/
+
@Override
public void changeColour_actionPerformed(String name)
{
*
* @param cs
*/
+
@Override
public void changeColour(ColourSchemeI cs)
{
/**
* Show the PID threshold slider panel
*/
+
@Override
protected void modifyPID_actionPerformed()
{
/**
* Show the Conservation slider panel
*/
+
@Override
protected void modifyConservation_actionPerformed()
{
/**
* Action on selecting or deselecting (Colour) By Conservation
*/
+
@Override
public void conservationMenuItem_actionPerformed(boolean selected)
{
/**
* Action on selecting or deselecting (Colour) Above PID Threshold
*/
+
@Override
public void abovePIDThreshold_actionPerformed(boolean selected)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void sortPairwiseMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void sortIDMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void sortLengthMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void sortGroupMenuItem_actionPerformed(ActionEvent e)
{
alignPanel.paintAlignment(true, false);
}
+
/**
* DOCUMENT ME!
*
* @param e
* DOCUMENT ME!
*/
+
@Override
public void removeRedundancyMenuItem_actionPerformed(ActionEvent e)
{
* @param e
* DOCUMENT ME!
*/
+
@Override
public void pairwiseAlignmentMenuItem_actionPerformed(ActionEvent e)
{
* @param options
* parameters for the distance or similarity calculation
*/
+
void newTreePanel(String type, String modelName,
SimilarityParamsI options)
{
* @param order
* DOCUMENT ME!
*/
+
public void addSortByOrderMenuItem(String title,
final AlignmentOrder order)
{
sort.add(item);
item.addActionListener(new java.awt.event.ActionListener()
{
+
@Override
public void actionPerformed(ActionEvent e)
{
* the label used to retrieve scores for each sequence on the
* alignment
*/
+
public void addSortByAnnotScoreMenuItem(JMenu sort,
final String scoreLabel)
{
sort.add(item);
item.addActionListener(new java.awt.event.ActionListener()
{
+
@Override
public void actionPerformed(ActionEvent e)
{
* rebuilding in subsequence calls.
*
*/
+
@Override
public void buildSortByAnnotationScoresMenu()
{
* closed, and adjust the tree leaf to sequence mapping when the alignment is
* modified.
*/
+
@Override
public void buildTreeSortMenu()
{
final JMenuItem item = new JMenuItem(tp.getTitle());
item.addActionListener(new java.awt.event.ActionListener()
{
+
@Override
public void actionPerformed(ActionEvent e)
{
}
return treePanels;
}
+
public boolean sortBy(AlignmentOrder alorder, String undoname)
{
SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
* be submitted for multiple alignment.
*
*/
+
public jalview.datamodel.AlignmentView gatherSequencesForAlignment()
{
// Now, check we have enough sequences
* region or the whole alignment. (where the first sequence in the set is the
* one that the prediction will be for).
*/
+
public AlignmentView gatherSeqOrMsaForSecStrPrediction()
{
AlignmentView seqs = null;
* @param e
* DOCUMENT ME!
*/
+
@Override
protected void loadTreeMenuItem_actionPerformed(ActionEvent e)
{
chooser.setResponseHandler(0, new Runnable()
{
+
@Override
public void run()
{
* position
* @return TreePanel handle
*/
+
public TreePanel showNewickTree(NewickFile nf, String treeTitle,
AlignmentView input, int w, int h, int x, int y)
{
if (nf.getTree() != null)
{
tp = new TreePanel(alignPanel, nf, treeTitle, input);
-
Dimension dim = Platform.getDimIfEmbedded(tp, -1, -1);
if (dim == null)
{
return tp;
}
-
+ private WebServicesMenuManager slivkaMenu = new WebServicesMenuManager("slivka", this);
/**
* Schedule the web services menu rebuild to the event dispatch thread.
*/
{
Console.info("Building web service menu for slivka");
SlivkaWSDiscoverer discoverer = SlivkaWSDiscoverer.getInstance();
- JMenu submenu = new JMenu("Slivka");
- buildWebServicesMenu(discoverer, submenu);
- webService.add(submenu);
+ slivkaMenu.setServices(discoverer);
+ slivkaMenu.setInProgress(discoverer.isRunning());
+ slivkaMenu.setNoServices(discoverer.isDone() && !discoverer.hasServices());
+ webService.add(slivkaMenu.getMenu());
}
if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
{
buildWebServicesMenu(jws2servs, submenu);
webService.add(submenu);
}
+ build_urlServiceMenu(webService);
build_fetchdbmenu(webService);
});
}
*
* @param webService
*/
+
protected void build_urlServiceMenu(JMenu webService)
{
// TODO: remove this code when 2.7 is released
*
* @return true if Show Cross-references menu should be enabled
*/
+
public boolean canShowProducts()
{
SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
JMenuItem xtype = new JMenuItem(source);
xtype.addActionListener(new ActionListener()
{
+
@Override
public void actionPerformed(ActionEvent e)
{
* @param source
* the database to show cross-references for
*/
+
protected void showProductsFor(final SequenceI[] sel, final boolean _odna,
final String source)
{
* Construct and display a new frame containing the translation of this
* frame's DNA sequences to their aligned protein (amino acid) equivalents.
*/
+
@Override
public void showTranslation_actionPerformed(GeneticCodeI codeTable)
{
*
* @param format
*/
+
public void setFileFormat(FileFormatI format)
{
this.currentFileFormat = format;
* access mode of file (see jalview.io.AlignFile)
* @return true if features file was parsed correctly.
*/
+
public boolean parseFeaturesFile(Object file, DataSourceType sourceType)
{
// BH 2018
+ " with " + toassoc.getDisplayId(true));
assocfiles++;
}
-
}
// TODO: do we need to update overview ? only if features are
// shown I guess
* @throws InterruptedException
* @throws IOException
*/
+
public void loadJalviewDataFile(Object file, DataSourceType sourceType,
FileFormatI format, SequenceI assocSeq)
{
}
if (isAnnotation)
{
-
updateForAnnotations();
}
} catch (Exception ex)
* Method invoked by the ChangeListener on the tabbed pane, in other words
* when a different tabbed pane is selected by the user or programmatically.
*/
+
@Override
public void tabSelectionChanged(int index)
{
/**
* On right mouse click on view tab, prompt for and set new view name.
*/
+
@Override
public void tabbedPane_mousePressed(MouseEvent e)
{
/**
* Open the dialog for regex description parsing.
*/
+
@Override
protected void extractScores_actionPerformed(ActionEvent e)
{
* jalview.jbgui.GAlignFrame#showDbRefs_actionPerformed(java.awt.event.ActionEvent
* )
*/
+
@Override
protected void showDbRefs_actionPerformed(ActionEvent e)
{
* @seejalview.jbgui.GAlignFrame#showNpFeats_actionPerformed(java.awt.event.
* ActionEvent)
*/
+
@Override
protected void showNpFeats_actionPerformed(ActionEvent e)
{
*
* @param av
*/
+
public boolean closeView(AlignViewportI av)
{
if (viewport == av)
Cache.getDefault(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES, true));
trimrs.addActionListener(new ActionListener()
{
+
@Override
public void actionPerformed(ActionEvent e)
{
{
new Thread(new Runnable()
{
+
@Override
public void run()
{
alignPanel.alignFrame.featureSettings, isNucleotide);
dbRefFetcher.addListener(new FetchFinishedListenerI()
{
+
@Override
public void finished()
{
rfetch.add(fetchr);
new Thread(new Runnable()
{
+
@Override
public void run()
{
// .getSequenceFetcherSingleton();
javax.swing.SwingUtilities.invokeLater(new Runnable()
{
+
@Override
public void run()
{
dbRefFetcher
.addListener(new FetchFinishedListenerI()
{
+
@Override
public void finished()
{
{ src.getDbSource() }));
fetchr.addActionListener(new ActionListener()
{
+
@Override
public void actionPerformed(ActionEvent e)
{
dbRefFetcher
.addListener(new FetchFinishedListenerI()
{
+
@Override
public void finished()
{
dbRefFetcher
.addListener(new FetchFinishedListenerI()
{
+
@Override
public void finished()
{
/**
* Left justify the whole alignment.
*/
+
@Override
protected void justifyLeftMenuItem_actionPerformed(ActionEvent e)
{
/**
* Right justify the whole alignment.
*/
+
@Override
protected void justifyRightMenuItem_actionPerformed(ActionEvent e)
{
* jalview.jbgui.GAlignFrame#showUnconservedMenuItem_actionPerformed(java.
* awt.event.ActionEvent)
*/
+
@Override
protected void showUnconservedMenuItem_actionPerformed(ActionEvent e)
{
* jalview.jbgui.GAlignFrame#showGroupConsensus_actionPerformed(java.awt.event
* .ActionEvent)
*/
+
@Override
protected void showGroupConsensus_actionPerformed(ActionEvent e)
{
* jalview.jbgui.GAlignFrame#showGroupConservation_actionPerformed(java.awt
* .event.ActionEvent)
*/
+
@Override
protected void showGroupConservation_actionPerformed(ActionEvent e)
{
* jalview.jbgui.GAlignFrame#showConsensusHistogram_actionPerformed(java.awt
* .event.ActionEvent)
*/
+
@Override
protected void showConsensusHistogram_actionPerformed(ActionEvent e)
{
* jalview.jbgui.GAlignFrame#showConsensusProfile_actionPerformed(java.awt
* .event.ActionEvent)
*/
+
@Override
protected void showSequenceLogo_actionPerformed(ActionEvent e)
{
* jalview.jbgui.GAlignFrame#makeGrpsFromSelection_actionPerformed(java.awt
* .event.ActionEvent)
*/
+
@Override
protected void makeGrpsFromSelection_actionPerformed(ActionEvent e)
{
*
* @param alignmentPanel
*/
+
public void setDisplayedView(AlignmentPanel alignmentPanel)
{
if (!viewport.getSequenceSetId()
* @param forAlignment
* update non-sequence-related annotations
*/
+
@Override
protected void setAnnotationsVisibility(boolean visible,
boolean forSequences, boolean forAlignment)
/**
* Store selected annotation sort order for the view and repaint.
*/
+
@Override
protected void sortAnnotations_actionPerformed()
{
*
* @return alignment panels in this alignment frame
*/
+
public List<? extends AlignmentViewPanel> getAlignPanels()
{
// alignPanels is never null
* Open a new alignment window, with the cDNA associated with this (protein)
* alignment, aligned as is the protein.
*/
+
protected void viewAsCdna_actionPerformed()
{
// TODO no longer a menu action - refactor as required
*
* @param show
*/
+
@Override
protected void showComplement_actionPerformed(boolean show)
{
* Generate the reverse (optionally complemented) of the selected sequences,
* and add them to the alignment
*/
+
@Override
protected void showReverse_actionPerformed(boolean complement)
{
* AlignFrame is set as currentAlignFrame in Desktop, to allow the script to
* be targeted at this alignment.
*/
+
@Override
protected void runGroovy_actionPerformed()
{
* @param columnsContaining
* @return
*/
+
public boolean hideFeatureColumns(String featureType,
boolean columnsContaining)
{
* Rebuilds the Colour menu, including any user-defined colours which have
* been loaded either on startup or during the session
*/
+
public void buildColourMenu()
{
colourMenu.removeAll();
* Open a dialog (if not already open) that allows the user to select and
* calculate PCA or Tree analysis
*/
+
protected void openTreePcaDialog()
{
if (alignPanel.getCalculationDialog() == null)
{
hmmerMenu.setEnabled(HmmerCommand.isHmmerAvailable());
}
+
@Override
protected void loadVcf_actionPerformed()
{
final AlignFrame us = this;
chooser.setResponseHandler(0, new Runnable()
{
+
@Override
public void run()
{
}
}
}
+
}
@Override
+ public void addProgressBar(long id, String message)
+ {
+ // TODO
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
public void removeProgressBar(long id)
{
//TODO
}
if (Cache.getDefault("SHOW_SLIVKA_SERVICES", true))
{
- tasks.add(jalview.ws.slivkaws.SlivkaWSDiscoverer.getInstance().startDiscoverer());
+ tasks.add(jalview.ws2.client.slivka.SlivkaWSDiscoverer
+ .getInstance().startDiscoverer());
}
if (blocking)
{
boolean operationInProgress();
/**
- * Remove progress bar with a given id from the panel.
+ * Adds a progress bar for the given id if it doesn't exist displaying the
+ * provided message. Subsequent calls do nothing.
*
* @param id
+ * progress bar identifier
+ * @param message
+ * displayed message
+ */
+ void addProgressBar(long id, String message);
+
+ /**
+ * Removes a progress bar for the given id if it exists. Subsequent calls do
+ * nothing.
+ *
+ * @param id
+ * id of the progress bar to be removed
*/
void removeProgressBar(long id);
+
}
}
@Override
+ public void addProgressBar(long id, String message)
+ {
+ progressBar.addProgressBar(id, message);
+ }
+
+ @Override
public void removeProgressBar(long id)
{
progressBar.removeProgressBar(id);
throw new NullPointerException();
}
if (!GridLayout.class
- .isAssignableFrom(container.getLayout().getClass()))
+ .isAssignableFrom(container.getLayout().getClass()))
{
throw new IllegalArgumentException("Container must have GridLayout");
}
@Override
public void run()
{
- JPanel progressPanel = progressBars.get(id);
- if (progressPanel != null)
+ if (progressBars.containsKey(id))
{
/*
* Progress bar is displayed for this id - remove it now, and any handler
*/
- progressBars.remove(id);
+ removeProgressBar(id);
if (message != null && statusBar != null)
{
statusBar.setText(message);
}
- if (progressBarHandlers.containsKey(id))
- {
- progressBarHandlers.remove(id);
- }
- removeRow(progressPanel);
}
else
{
/*
* No progress bar for this id - add one now
*/
- progressPanel = new JPanel(new BorderLayout(10, 5));
-
- JProgressBar progressBar = new JProgressBar();
- progressBar.setIndeterminate(true);
-
- progressPanel.add(new JLabel(message), BorderLayout.WEST);
- progressPanel.add(progressBar, BorderLayout.CENTER);
-
- addRow(progressPanel);
-
- progressBars.put(id, progressPanel);
+ addProgressBar(id, message);
}
-
- refreshLayout();
}
});
}
-
+
+ /**
+ * Add a progress bar for the given id if it doesn't exist displaying the
+ * provided message. Subsequent calls do nothing.
+ *
+ * @param id
+ * progress bar identifier
+ * @param message
+ * displayed message
+ */
+ @Override
+ public void addProgressBar(final long id, final String message)
+ {
+ if (progressBars.containsKey(id))
+ return;
+ JPanel progressPanel = new JPanel(new BorderLayout(10, 5));
+ progressBars.put(id, progressPanel);
+ Runnable r = () -> {
+ JProgressBar progressBar = new JProgressBar();
+ progressBar.setIndeterminate(true);
+ progressPanel.add(new JLabel(message), BorderLayout.WEST);
+ progressPanel.add(progressBar, BorderLayout.CENTER);
+ addRow(progressPanel);
+ refreshLayout();
+ };
+ if (SwingUtilities.isEventDispatchThread())
+ r.run();
+ else
+ SwingUtilities.invokeLater(r);
+ }
+
+ /**
+ * Remove a progress bar for the given id if it exists. Subsequent calls do
+ * nothing.
+ *
+ * @param id
+ * id of the progress bar to be removed
+ */
@Override
public void removeProgressBar(final long id)
{
- SwingUtilities.invokeLater(() -> {
- JPanel progressPanel = progressBars.get(id);
- if (progressPanel != null)
- {
- progressBars.remove(id);
- if (progressBarHandlers.containsKey(id))
- {
- progressBarHandlers.remove(id);
- }
- removeRow(progressPanel);
- }
- });
+ JPanel progressPanel = progressBars.remove(id);
+ if (progressPanel == null)
+ return;
+ progressBarHandlers.remove(id);
+ Runnable r = () -> {
+ removeRow(progressPanel);
+ refreshLayout();
+ };
+ if (SwingUtilities.isEventDispatchThread())
+ r.run();
+ else
+ SwingUtilities.invokeLater(r);
}
/**
*/
@Override
public void registerHandler(final long id,
- final IProgressIndicatorHandler handler)
+ final IProgressIndicatorHandler handler)
{
final IProgressIndicator us = this;
if (progressPanel == null)
{
System.err.println(
- "call setProgressBar before registering the progress bar's handler.");
+ "call setProgressBar before registering the progress bar's handler.");
return;
}
progressBarHandlers.put(id, handler);
JButton cancel = new JButton(
- MessageManager.getString("action.cancel"));
+ MessageManager.getString("action.cancel"));
cancel.addActionListener(new ActionListener()
{
{
handler.cancelActivity(id);
us.setProgressBar(MessageManager
- .formatMessage("label.cancelled_params", new Object[]
- { ((JLabel) progressPanel.getComponent(0)).getText() }),
- id);
+ .formatMessage("label.cancelled_params", new Object[]
+ { ((JLabel) progressPanel.getComponent(0)).getText() }),
+ id);
}
});
progressPanel.add(cancel, BorderLayout.EAST);
import jalview.bin.Cache;
import jalview.bin.Console;
import jalview.util.MessageManager;
-import jalview.ws.WSDiscovererI;
-import jalview.ws.slivkaws.SlivkaWSDiscoverer;
+import jalview.ws2.client.api.WebServiceDiscovererI;
+import jalview.ws2.client.slivka.SlivkaWSDiscoverer;
import java.awt.BorderLayout;
import java.awt.Color;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import javax.swing.BorderFactory;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
+
@SuppressWarnings("serial")
public class SlivkaPreferences extends JPanel
{
setPreferredSize(new Dimension(500, 450));
}
- WSDiscovererI discoverer;
+ WebServiceDiscovererI discoverer;
- private final ArrayList<String> urls = new ArrayList<>();
+ private final ArrayList<URL> urls = new ArrayList<>();
- private final Map<String, Integer> statuses = new HashMap<>();
+ private final Map<URL, Integer> statuses = new HashMap<>();
private final AbstractTableModel urlTableModel = new AbstractTableModel()
{
switch (columnIndex)
{
case 0:
- return urls.get(rowIndex);
+ return urls.get(rowIndex).toString();
case 1:
- return statuses.getOrDefault(urls.get(rowIndex), WSDiscovererI.STATUS_UNKNOWN);
+ return statuses.getOrDefault(urls.get(rowIndex), WebServiceDiscovererI.STATUS_UNKNOWN);
default:
throw new NoSuchElementException();
}
hasFocus, row, column);
switch ((Integer) value)
{
- case WSDiscovererI.STATUS_NO_SERVICES:
+ case WebServiceDiscovererI.STATUS_NO_SERVICES:
setForeground(Color.ORANGE);
break;
- case WSDiscovererI.STATUS_OK:
+ case WebServiceDiscovererI.STATUS_OK:
setForeground(Color.GREEN);
break;
- case WSDiscovererI.STATUS_INVALID:
+ case WebServiceDiscovererI.STATUS_INVALID:
setForeground(Color.RED);
break;
- case WSDiscovererI.STATUS_UNKNOWN:
+ case WebServiceDiscovererI.STATUS_UNKNOWN:
default:
setForeground(Color.LIGHT_GRAY);
}
JButton moveUrlDown = new JButton(
MessageManager.getString("action.move_down"));
- private String showEditUrlDialog(String oldUrl)
+ private URL showEditUrlDialog(String oldUrl)
{
String input = (String) JvOptionPane
.showInternalInputDialog(
}
try
{
- new URL(input);
+ return new URL(input);
} catch (MalformedURLException ex)
{
JvOptionPane.showInternalMessageDialog(this,
JOptionPane.WARNING_MESSAGE);
return null;
}
- return input;
}
// Button Action Listeners
private ActionListener newUrlAction = (ActionEvent e) -> {
- final String input = showEditUrlDialog("");
+ final URL input = showEditUrlDialog("");
if (input != null)
{
urls.add(input);
reloadStatusForUrl(input);
urlTableModel.fireTableRowsInserted(urls.size(), urls.size());
- discoverer.setServiceUrls(urls);
+ discoverer.setUrls(urls);
}
};
final int i = urlListTable.getSelectedRow();
if (i >= 0)
{
- final String input = showEditUrlDialog(urls.get(i));
+ final URL input = showEditUrlDialog(urls.get(i).toString());
if (input != null)
{
urls.set(i, input);
statuses.remove(input);
reloadStatusForUrl(input);
urlTableModel.fireTableRowsUpdated(i, i);
- discoverer.setServiceUrls(urls);
+ discoverer.setUrls(urls);
}
}
};
urls.remove(i);
statuses.remove(i);
urlTableModel.fireTableRowsDeleted(i, i);
- discoverer.setServiceUrls(urls);
+ discoverer.setUrls(urls);
}
};
if (i > 0)
{
moveTableRow(i, i - 1);
- discoverer.setServiceUrls(urls);
+ discoverer.setUrls(urls);
}
};
if (i >= 0 && i < urls.size() - 1)
{
moveTableRow(i, i + 1);
- discoverer.setServiceUrls(urls);
+ discoverer.setUrls(urls);
}
};
private void moveTableRow(int fromIndex, int toIndex)
{
- String url = urls.get(fromIndex);
+ URL url = urls.get(fromIndex);
int status = statuses.get(fromIndex);
urls.set(fromIndex, urls.get(toIndex));
urls.set(toIndex, url);
private ActionListener refreshServicesAction = (ActionEvent e) -> {
progressBar.setVisible(true);
Console.info("Requesting service reload");
- discoverer.startDiscoverer().handle((_discoverer, exception) -> {
+ discoverer.startDiscoverer().handle((services, exception) -> {
if (exception == null)
{
Console.info("Reloading done");
}
- else
+ else if (exception instanceof CancellationException)
{
+ Console.info("Reloading cancelled");
+ }
+ else {
Console.error("Reloading failed", exception);
}
SwingUtilities.invokeLater(() -> progressBar.setVisible(false));
};
private ActionListener resetServicesAction = (ActionEvent e) -> {
- discoverer.setServiceUrls(null);
+ discoverer.setUrls(null);
urls.clear();
statuses.clear();
- urls.addAll(discoverer.getServiceUrls());
- for (String url : urls)
+ urls.addAll(discoverer.getUrls());
+ for (URL url : urls)
{
reloadStatusForUrl(url);
}
{
// Initial URLs loading
discoverer = SlivkaWSDiscoverer.getInstance();
- urls.addAll(discoverer.getServiceUrls());
- for (String url : urls)
+ urls.addAll(discoverer.getUrls());
+ for (URL url : urls)
{
reloadStatusForUrl(url);
}
}
- private void reloadStatusForUrl(String url)
+ private void reloadStatusForUrl(URL url)
{
- CompletableFuture.supplyAsync(() -> discoverer.getServerStatusFor(url))
+ CompletableFuture.supplyAsync(() -> discoverer.getStatusForUrl(url))
.thenAccept((status) -> {
statuses.put(url, status);
int row = urls.indexOf(url);
}
@Override
+ public void addProgressBar(long id, String message)
+ {
+ progressBar.addProgressBar(id, message);
+ }
+
+ @Override
public void removeProgressBar(long id)
{
progressBar.removeProgressBar(id);
}
@Override
+ public void addProgressBar(long id, String message)
+ {
+ progressBar.addProgressBar(id, message);
+ }
+
+ @Override
public void removeProgressBar(long id)
{
progressBar.removeProgressBar(id);
jbInit();
this.paramStore = store;
this.service = null;
- init(preset, args);
+ initForService(preset, args);
validate();
}
*/
package jalview.util;
+import java.util.Objects;
+
public class ArrayUtils
{
/**
}
}
}
+
+ /**
+ * Return the index of the first occurrence of the item in the array or -1 if
+ * the array doesn't contain the item.
+ *
+ * @param arr
+ * array to be searched
+ * @param item
+ * item to search for
+ * @return index of the first occurrence of the item or -1 if not found
+ */
+ public static int indexOf(Object[] arr, Object item)
+ {
+ for (int i = 0; i < arr.length; i++)
+ {
+ if (Objects.equals(arr[i], item))
+ return i;
+ }
+ return -1;
+ }
}
return gcd(b, a % b);
}
+
+ private static int uidCounter = (int)(Math.random() * 0xffffffff);
+ /**
+ * Generates a unique 64-bit identifier.
+ */
+ public static long getUID()
+ {
+ long uid = 0L;
+ uid |= ((System.currentTimeMillis() >> 10) & 0xfffffffL) << 36;
+ uid |= (long)(Math.random() * 0xfL) << 32;
+ uid |= ++uidCounter & 0xffffffff;
+ return uid;
+ }
}
--- /dev/null
+package jalview.util;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A generic immutable pair of values.
+ *
+ * @author mmwarowny
+ *
+ * @param <T>
+ * first value type
+ * @param <U>
+ * second value type
+ */
+public class Pair<T, U> implements Iterable<Object>
+{
+ final T val0;
+
+ final U val1;
+
+ public Pair(T val0, U val1)
+ {
+ this.val0 = val0;
+ this.val1 = val1;
+ }
+
+ /**
+ * Return value at the specified index cast to type R
+ * @param <R> return type
+ * @param index item index
+ * @return value at given index
+ * @throws ClassCastException value cannot be cast to R
+ * @throws IndexOutOfBoundsException index outside tuple size
+ */
+ @SuppressWarnings("unchecked")
+ public <R> R get(int index) throws ClassCastException, IndexOutOfBoundsException
+ {
+ if (index == 0)
+ return (R) val0;
+ if (index == 1)
+ return (R) val1;
+ throw new IndexOutOfBoundsException(index);
+ }
+
+ /**
+ * @return 0th value of the pair
+ */
+ public T get0()
+ {
+ return val0;
+ }
+
+ /**
+ * @return 1st value of the pair
+ */
+ public U get1()
+ {
+ return val1;
+ }
+
+ /**
+ * @return tuple size
+ */
+ public int size()
+ {
+ return 2;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (obj instanceof Pair)
+ {
+ Pair<?, ?> other = (Pair<?, ?>) obj;
+ return Objects.equals(val0, other.val0) &&
+ Objects.equals(val1, other.val1);
+ }
+ return false;
+ }
+
+ @Override
+ public Iterator<Object> iterator()
+ {
+ return List.of(val0, val1).iterator();
+ }
+}
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
/**
* base class holding visualization and analysis attributes and common logic for
return false;
}
+ private ScheduledExecutorService serviceExecutor = Executors.newSingleThreadScheduledExecutor();
+
+ /**
+ * Get a default scheduled executor service which can be used by
+ * services and calculators to run parallel jobs associated with this
+ * viewport.
+ *
+ * @return default service executor of that viewport
+ */
+ public ScheduledExecutorService getServiceExecutor()
+ {
+ return serviceExecutor;
+ }
+
public void setAlignment(AlignmentI align)
{
this.alignment = align;
gapcounts = null;
calculator.shutdown();
calculator = null;
+ serviceExecutor.shutdown();
+ serviceExecutor = null;
residueShading = null; // may hold a reference to Consensus
changeSupport = null;
ranges = null;
task = executor.submit(() -> {
try
{
- Console.debug(format("Worker %s started",
- getWorker().getClass().getName()));
+ Console.debug(format("Worker %s started", getWorker()));
getWorker().run();
- Console.debug(format("Worker %s finished",
- getWorker().getClass().getName()));
+ Console.debug(format("Worker %s finished", getWorker()));
} catch (InterruptedException e)
{
- Console.debug(format("Worker %s interrupted",
- getWorker().getClass().getName()));
+ Console.debug(format("Worker %s interrupted", getWorker()));
} catch (Throwable th)
{
- Console.debug(format("Worker %s failed",
- getWorker().getClass().getName()), th);
+ Console.debug(format("Worker %s failed", getWorker()), th);
} finally
{
if (!isRegistered())
{
return;
}
- Console.debug(format("Cancelling worker %s",
- getWorker().getClass().getName()));
+ Console.debug(format("Cancelling worker %s", getWorker()));
task.cancel(true);
}
}
"Cannot submit new task if the prevoius one is still running");
}
Console.debug(
- format("Worker %s queued", getWorker().getClass().getName()));
+ format("Worker %s queued", getWorker()));
final var runnable = new Runnable()
{
private boolean started = false;
{
if (!started)
{
- Console.debug(format("Worker %s started",
- getWorker().getClass().getName()));
+ Console.debug(format("Worker %s started", getWorker()));
getWorker().startUp();
started = true;
}
else if (!completed)
{
- Console.debug(format("Polling worker %s",
- getWorker().getClass().getName()));
+ Console.debug(format("Polling worker %s", getWorker()));
if (getWorker().poll())
{
- Console.debug(format("Worker %s finished",
- getWorker().getClass().getName()));
+ Console.debug(format("Worker %s finished", getWorker()));
completed = true;
}
}
} catch (Throwable th)
{
- Console.debug(format("Worker %s failed",
- getWorker().getClass().getName()), th);
+ Console.debug(format("Worker %s failed", getWorker()), th);
completed = true;
}
if (completed)
final var worker = getWorker();
if (!isRegistered())
PollableWorkerManager.super.worker = null;
- Console.debug(format("Finalizing completed worker %s",
- worker.getClass().getName()));
+ Console.debug(format("Finalizing completed worker %s", worker));
worker.done();
// almost impossible, but the future may be null at this point
// let it throw NPE to cancel forcefully
{
return;
}
- Console.debug(format("Cancelling worker %s",
- getWorker().getClass().getName()));
+ Console.debug(format("Cancelling worker %s", getWorker()));
task.cancel(false);
executor.submit(() -> {
final var worker = getWorker();
if (worker != null)
{
worker.cancel();
- Console.debug(format("Finalizing cancelled worker %s",
- worker.getClass().getName()));
+ Console.debug(format("Finalizing cancelled worker %s", worker));
worker.done();
}
});
public WsParamSetI getPreset(String name);
+ /**
+ * Returns if the service has presets.
+ * @return {@code true} if service has presets
+ */
+ public default boolean hasPresets()
+ {
+ var presets = getPresets();
+ return presets != null && presets.size() > 0;
+ }
+
public List<ArgumentI> getServiceParameters();
+ /**
+ * Returns if the service has parameters.
+ * @return {@code true} if service has parameters
+ */
+ public default boolean hasParameters()
+ {
+ var parameters = getServiceParameters();
+ return parameters != null && parameters.size() > 0;
+ }
+
public boolean presetExists(String name);
public void deletePreset(String name);
import java.net.URL;
import java.util.Arrays;
+import java.util.List;
+
+import static java.util.Objects.requireNonNullElse;
public class BooleanOption extends Option
{
+ public static class Builder extends Option.Builder
+ {
+ private boolean defaultValue = false;
+
+ private boolean value = false;
+
+ private String reprValue = null;
+
+ public void setDefaultValue(Boolean defaultValue)
+ {
+ this.defaultValue = requireNonNullElse(defaultValue, false);
+ }
+
+ public void setValue(Boolean value)
+ {
+ this.value = requireNonNullElse(value, false);
+ }
+
+ public void setReprValue(String reprValue)
+ {
+ this.reprValue = reprValue;
+ }
+
+ @Override
+ public void setPossibleValues(List<String> possibleValues)
+ {
+ throw new UnsupportedOperationException("cannot set possible values for boolean");
+ }
+
+ @Override
+ public BooleanOption build()
+ {
+ return new BooleanOption(this);
+ }
+ }
+
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
+ protected BooleanOption(Builder builder)
+ {
+ super(builder);
+ String reprValue = requireNonNullElse(builder.reprValue, name);
+ defvalue = builder.defaultValue ? reprValue : null;
+ value = builder.value ? reprValue : null;
+ possibleVals = List.of(reprValue);
+ displayVals = List.of(label);
+ }
public BooleanOption(String name, String descr, boolean required,
Boolean defVal, Boolean val, URL link)
import jalview.ws.params.ParameterI;
import jalview.ws.params.ValueConstrainI;
+import static java.util.Objects.requireNonNullElse;
/**
*
*/
public class DoubleParameter extends Option implements ParameterI
{
+ public static class Builder extends Option.Builder
+ {
+ // setting them the opposite way disables limits until both are set.
+ protected double min = Double.POSITIVE_INFINITY;
+
+ protected double max = Double.NEGATIVE_INFINITY;
+
+ /**
+ * Setting string on double parameter is not allowed, use
+ * {@link #setValue(Double)} instead.
+ */
+ @Override
+ public void setValue(String value)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setValue(Double value)
+ {
+ if (value != null)
+ super.setValue(value.toString());
+ else
+ super.setValue(null);
+ }
+
+ /**
+ * Setting string on double parameter is not allowed, use
+ * {@link #setDefaultValue(Double)} instead.
+ */
+ @Override
+ public void setDefaultValue(String defaultValue)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setDefaultValue(Double defaultValue)
+ {
+ if (defaultValue != null)
+ super.setDefaultValue(defaultValue.toString());
+ else
+ super.setDefaultValue(null);
+ }
+
+ public void setMin(Double min)
+ {
+ this.min = requireNonNullElse(min, Double.POSITIVE_INFINITY);
+ }
+
+ public void setMax(Double max)
+ {
+ this.max = requireNonNullElse(max, Double.NEGATIVE_INFINITY);
+ }
+
+ public void setBounds(Double min, Double max)
+ {
+ setMin(min);
+ setMax(max);
+ }
+
+ public DoubleParameter build()
+ {
+ return new DoubleParameter(this);
+ }
+ }
+
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
double defval;
double min;
};
}
+ protected DoubleParameter(Builder builder)
+ {
+ super(builder);
+ this.min = builder.min;
+ this.max = builder.max;
+ if (defvalue != null)
+ defval = Double.parseDouble(defvalue);
+ }
+
public DoubleParameter(DoubleParameter parm)
{
super(parm);
}
public DoubleParameter(String name, String description, boolean required,
- Double defValue, double min, double max)
+ Double defValue, double min, double max)
{
super(name, description, required, String.valueOf(defValue), null, null,
- null);
+ null);
defval = defValue;
this.min = min;
this.max = max;
}
public DoubleParameter(String name, String description, boolean required,
- Double defValue, Double value, double min, double max)
+ Double defValue, Double value, double min, double max)
{
super(name, description, required, String.valueOf(defValue),
- String.valueOf(value), null, null);
+ String.valueOf(value), null, null);
defval = defValue;
this.min = min;
this.max = max;
*/
public class FileParameter extends StringParameter
{
+ public static class Builder extends StringParameter.Builder
+ {
+ @Override
+ public FileParameter build()
+ {
+ return new FileParameter(this);
+ }
+ }
+
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
+ public FileParameter(Builder builder)
+ {
+ super(builder);
+ }
public FileParameter(String name, String description, boolean required,
- String defValue, String value)
+ String defValue, String value)
{
super(name, description, required, defValue, value);
}
import jalview.ws.params.ParameterI;
import jalview.ws.params.ValueConstrainI;
+import static java.util.Objects.requireNonNullElse;
/**
* @author jimp
*/
public class IntegerParameter extends Option implements ParameterI
{
+ public static class Builder extends Option.Builder
+ {
+ // assigning them the opposite way results in no limits unless both are set
+ protected int min = Integer.MAX_VALUE;
+
+ protected int max = Integer.MIN_VALUE;
+
+ @Override
+ public void setDefaultValue(String defaultValue)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setDefaultValue(Integer defaultValue)
+ {
+ if (defaultValue != null)
+ super.setDefaultValue(defaultValue.toString());
+ else
+ super.setDefaultValue(null);
+ }
+
+ @Override
+ public void setValue(String value)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setValue(Integer value)
+ {
+ if (value != null)
+ super.setValue(value.toString());
+ else
+ super.setValue(null);
+ }
+
+ public void setMin(Integer min)
+ {
+ this.min = requireNonNullElse(min, Integer.MAX_VALUE);
+ }
+
+ public void setMax(Integer max)
+ {
+ this.max = requireNonNullElse(max, Integer.MIN_VALUE);
+ }
+
+ public void setBounds(Integer min, Integer max)
+ {
+ setMin(min);
+ setMax(max);
+ }
+
+ public IntegerParameter build()
+ {
+ return new IntegerParameter(this);
+ }
+ }
+
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
int defval;
int min;
};
}
+ protected IntegerParameter(Builder builder)
+ {
+ super(builder);
+ min = builder.min;
+ max = builder.max;
+ if (defvalue != null)
+ defval = Integer.parseInt(defvalue);
+ }
+
public IntegerParameter(IntegerParameter parm)
{
super(parm);
}
public IntegerParameter(String name, String description, boolean required,
- int defValue, int min, int max)
+ int defValue, int min, int max)
{
super(name, description, required, String.valueOf(defValue), null, null,
- null);
+ null);
defval = defValue;
this.min = min;
this.max = max;
}
public IntegerParameter(String name, String description, boolean required,
- int defValue, int value, int min, int max)
+ int defValue, int value, int min, int max)
{
super(name, description, required, String.valueOf(defValue),
- String.valueOf(value), null, null);
+ String.valueOf(value), null, null);
defval = defValue;
this.min = min;
this.max = max;
package jalview.ws.params.simple;
+import static java.util.Objects.requireNonNullElse;
+
import jalview.ws.params.ParameterI;
import jalview.ws.params.ValueConstrainI;
*/
public class LogarithmicParameter extends Option implements ParameterI
{
+ public static class Builder extends Option.Builder
+ {
+ // setting them the opposite way disables limits until both are set.
+ protected double min = Double.POSITIVE_INFINITY;
+
+ protected double max = Double.NEGATIVE_INFINITY;
+
+ /**
+ * Setting string on double parameter is not allowed, use
+ * {@link #setValue(Double)} instead.
+ */
+ @Override
+ public void setValue(String value)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setValue(Double value)
+ {
+ if (value != null)
+ super.setValue(value.toString());
+ else
+ super.setValue(null);
+ }
+
+ /**
+ * Setting string on double parameter is not allowed, use
+ * {@link #setDefaultValue(Double)} instead.
+ */
+ @Override
+ public void setDefaultValue(String defaultValue)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setDefaultValue(Double defaultValue)
+ {
+ if (defaultValue != null)
+ super.setDefaultValue(defaultValue.toString());
+ else
+ super.setDefaultValue(null);
+ }
+
+ public void setMin(Double min)
+ {
+ this.min = requireNonNullElse(min, Double.POSITIVE_INFINITY);
+ }
+
+ public void setMax(Double max)
+ {
+ this.max = requireNonNullElse(max, Double.NEGATIVE_INFINITY);
+ }
+
+ public void setBounds(Double min, Double max)
+ {
+ setMin(min);
+ setMax(max);
+ }
+
+ @Override
+ public LogarithmicParameter build()
+ {
+ return new LogarithmicParameter(this);
+ }
+ }
+
final double defval;
final double min;
};
}
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
+ public LogarithmicParameter(Builder builder)
+ {
+ super(builder);
+ this.min = builder.min;
+ this.max = builder.max;
+ if (defvalue != null)
+ defval = Double.parseDouble(defvalue);
+ else
+ defval = 0.0;
+ }
+
public LogarithmicParameter(LogarithmicParameter parm)
{
super(parm);
}
public LogarithmicParameter(String name, String description,
- boolean required, Double defValue, double min, double max)
+ boolean required, Double defValue, double min, double max)
{
super(name, description, required, String.valueOf(defValue), null, null,
- null);
+ null);
defval = defValue;
this.min = min;
this.max = max;
}
public LogarithmicParameter(String name, String description,
- boolean required, Double defValue, double value, double min,
- double max)
+ boolean required, Double defValue, double value, double min,
+ double max)
{
super(name, description, required, String.valueOf(defValue),
- String.valueOf(value), null, null);
+ String.valueOf(value), null, null);
defval = defValue;
this.min = min;
this.max = max;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
+import static java.util.Objects.requireNonNull;
+import static java.util.Objects.requireNonNullElse;
public class Option implements OptionI
{
+ /**
+ * A builder class which avoids multiple telescoping parameters nightmare.
+ *
+ * @author mmwarowny
+ *
+ */
+ public static class Builder
+ {
+ protected String name = null;
+
+ protected String label = null;
+
+ protected String description = "";
+
+ protected boolean required = false;
+
+ protected String defaultValue = null;
+
+ protected String value = null;
+
+ protected List<String> possibleValues = null;
+
+ protected List<String> displayValues = null;
+
+ protected URL detailsUrl = null;
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public void setLabel(String label)
+ {
+ this.label = label;
+ }
+
+ public void setDescription(String description)
+ {
+ this.description = description;
+ }
+
+ public void setRequired(boolean required)
+ {
+ this.required = required;
+ }
+
+ public void setDefaultValue(String defaultValue)
+ {
+ this.defaultValue = defaultValue;
+ }
+
+ public void setValue(String value)
+ {
+ this.value = value;
+ }
+
+ public void setPossibleValues(List<String> possibleValues)
+ {
+ this.possibleValues = possibleValues;
+ }
+
+ public void setDisplayValues(List<String> displayValues)
+ {
+ this.displayValues = displayValues;
+ }
+
+ public void setDetailsUrl(URL detailsUrl)
+ {
+ this.detailsUrl = detailsUrl;
+ }
+
+ public Option build()
+ {
+ return new Option(this);
+ }
+ }
+
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
String name;
String label;
URL fdetails;
+ protected Option(Builder builder)
+ {
+ requireNonNull(builder.name);
+ name = builder.name;
+ label = requireNonNullElse(builder.label, name);
+ description = builder.description;
+ required = builder.required;
+ defvalue = builder.defaultValue;
+ value = builder.value;
+ if (builder.possibleValues != null)
+ possibleVals = new ArrayList<>(builder.possibleValues);
+ if (builder.displayValues != null)
+ displayVals = new ArrayList<>(builder.displayValues);
+ else
+ displayVals = possibleVals;
+ if (possibleVals == null && displayVals != null)
+ throw new IllegalArgumentException(
+ "cannot use displayValues if possibleValues is null");
+ if (possibleVals != null && possibleVals.size() != displayVals.size())
+ throw new IllegalArgumentException(
+ "displayValues size does not match possibleValues");
+ fdetails = builder.detailsUrl;
+ }
+
/**
* Copy constructor
*
* @param fdetails
*/
public Option(String name2, String description2, boolean isrequired,
- String defValue, String val, List<String> possibleVals,
- List<String> displayNames, URL fdetails)
+ String defValue, String val, List<String> possibleVals,
+ List<String> displayNames, URL fdetails)
{
name = name2;
description = description2;
* @param fdetails
*/
public Option(String name2, String description2, boolean isrequired,
- String defValue, String val, List<String> possibleVals,
- URL fdetails)
+ String defValue, String val, List<String> possibleVals,
+ URL fdetails)
{
this(name2, description2, isrequired, defValue, val, possibleVals, null,
- fdetails);
+ fdetails);
}
@Override
*/
public class RadioChoiceParameter extends StringParameter
{
+ public static class Builder extends StringParameter.Builder
+ {
+ @Override
+ public RadioChoiceParameter build()
+ {
+ return new RadioChoiceParameter(this);
+ }
+ }
+
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
+ public RadioChoiceParameter(Builder builder)
+ {
+ super(builder);
+ }
/**
* Constructor
* @param def
*/
public RadioChoiceParameter(String name, String description,
- List<String> options, String def)
+ List<String> options, String def)
{
super(name, description, true, def, def, options, null);
}
public class StringParameter extends Option implements ParameterI
{
+ public static class Builder extends Option.Builder
+ {
+ @Override
+ public StringParameter build()
+ {
+ return new StringParameter(this);
+ }
+ }
+
@Override
public ValueConstrainI getValidValue()
{
}
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
+ protected StringParameter(Builder builder)
+ {
+ super(builder);
+ }
+
public StringParameter(StringParameter parm)
{
this.name = parm.name;
}
public StringParameter(String name, String description, boolean required,
- String defValue)
+ String defValue)
{
super(name, description, required, String.valueOf(defValue), null, null,
- null);
+ null);
this.defvalue = defValue;
}
public StringParameter(String name, String description, boolean required,
- String defValue, String value)
+ String defValue, String value)
{
super(name, description, required, String.valueOf(defValue),
- String.valueOf(value), null, null);
+ String.valueOf(value), null, null);
this.defvalue = defValue;
}
* @param displayNames
*/
public StringParameter(String name2, String description2,
- boolean isrequired, String defValue, String value,
- List<String> possibleVals, List<String> displayNames)
+ boolean isrequired, String defValue, String value,
+ List<String> possibleVals, List<String> displayNames)
{
super(name2, description2, isrequired, defValue, value, possibleVals,
- displayNames, null);
+ displayNames, null);
}
}
--- /dev/null
+package jalview.ws2.actions;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import jalview.bin.Cache;
+import jalview.util.ArrayUtils;
+import jalview.util.MathUtils;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.api.WebServiceJobHandle;
+import jalview.ws2.client.api.WebServiceClientI;
+import jalview.ws2.helpers.DelegateJobEventListener;
+import jalview.ws2.helpers.TaskEventSupport;
+import static java.lang.String.format;
+
+/**
+ * An abstract base class for non-interactive tasks which implements common
+ * tasks methods. Additionally, it manages task execution in a polling loop.
+ * Subclasses are only required to implement {@link #prepare()} and
+ * {@link #done()} methods.
+ *
+ * @author mmwarowny
+ *
+ * @param <T>
+ * the type of jobs managed by the task
+ * @param <R>
+ * the type of result provided by the task
+ */
+public abstract class AbstractPollableTask<T extends BaseJob, R> implements TaskI<R>
+{
+ private final long uid = MathUtils.getUID();
+
+ protected final WebServiceClientI client;
+
+ protected final List<ArgumentI> args;
+
+ protected final Credentials credentials;
+
+ private final TaskEventSupport<R> eventHandler;
+
+ protected JobStatus taskStatus = null;
+
+ private Future<?> future = null;
+
+ protected List<T> jobs = Collections.emptyList();
+
+ protected R result;
+
+ protected AbstractPollableTask(WebServiceClientI client, List<ArgumentI> args,
+ Credentials credentials, TaskEventListener<R> eventListener)
+ {
+ this.client = client;
+ this.args = args;
+ this.credentials = credentials;
+ this.eventHandler = new TaskEventSupport<R>(this, eventListener);
+ }
+
+ public long getUid()
+ {
+ return uid;
+ }
+
+ /**
+ * Start the task using provided scheduled executor service. It creates a
+ * polling loop running at set intervals.
+ *
+ * @param executor
+ * executor to run the polling loop with
+ */
+ public void start(ScheduledExecutorService executor)
+ {
+ if (future != null)
+ throw new IllegalStateException("task already started");
+ var runnable = new Runnable()
+ {
+ private int stage = STAGE_PREPARE;
+
+ private static final int STAGE_PREPARE = 0;
+
+ private static final int STAGE_START = 1;
+
+ private static final int STAGE_POLL = 2;
+
+ private static final int STAGE_FINALIZE = 3;
+
+ private static final int STAGE_DONE = 4;
+
+ private int retryCount = 0;
+
+ private static final int MAX_RETRY = 5;
+
+ /**
+ * A polling loop run periodically which carries the task through its
+ * consecutive execution stages.
+ */
+ @Override
+ public void run()
+ {
+ if (stage == STAGE_PREPARE)
+ {
+ // first stage - the input data is collected and the jobs are created
+ try
+ {
+ jobs = prepare();
+ } catch (ServiceInputInvalidException e)
+ {
+ stage = STAGE_DONE;
+ setStatus(JobStatus.INVALID);
+ eventHandler.fireTaskException(e);
+ throw new CompletionException(e);
+ }
+ stage = STAGE_START;
+ setStatus(JobStatus.READY);
+ eventHandler.fireTaskStarted(jobs);
+ var jobListener = new DelegateJobEventListener<>(eventHandler);
+ for (var job : jobs)
+ {
+ job.addPropertyChagneListener(jobListener);
+ }
+ }
+ try
+ {
+ if (stage == STAGE_START)
+ {
+ // second stage - jobs are submitted to the server
+ startJobs();
+ stage = STAGE_POLL;
+ setStatus(JobStatus.SUBMITTED);
+ }
+ if (stage == STAGE_POLL)
+ {
+ // third stage - jobs are poolled until all of them are completed
+ if (pollJobs())
+ {
+ stage = STAGE_FINALIZE;
+ }
+ updateGlobalStatus();
+ }
+ if (stage == STAGE_FINALIZE)
+ {
+ // final stage - results are collected and stored
+ result = done();
+ eventHandler.fireTaskCompleted(result);
+ stage = STAGE_DONE;
+ }
+ retryCount = 0;
+ } catch (IOException e)
+ {
+ eventHandler.fireTaskException(e);
+ if (++retryCount > MAX_RETRY)
+ {
+ stage = STAGE_DONE;
+ cancelJobs();
+ setStatus(JobStatus.SERVER_ERROR);
+ throw new CompletionException(e);
+ }
+ }
+ if (stage == STAGE_DONE)
+ {
+ // finalization - terminating the future task
+ throw new CancellationException("task terminated");
+ }
+ }
+ };
+ if (taskStatus != JobStatus.CANCELLED)
+ future = executor.scheduleWithFixedDelay(runnable, 0, 2, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public JobStatus getStatus()
+ {
+ return taskStatus;
+ }
+
+ /**
+ * Set the status of the task and notify the event handler.
+ *
+ * @param status
+ * new task status
+ */
+ protected void setStatus(JobStatus status)
+ {
+ if (this.taskStatus != status)
+ {
+ this.taskStatus = status;
+ eventHandler.fireTaskStatusChanged(status);
+ }
+ }
+
+ /**
+ * Update task status according to the overall status of its jobs. The rules
+ * of setting the status are following:
+ * <ul>
+ * <li>task is invalid if all jobs are invalid</li>
+ * <li>task is completed if all but invalid jobs are completed</li>
+ * <li>task is ready, submitted or queued if at least one job is ready,
+ * submitted or queued an none proceeded to the next stage excluding
+ * completed.</li>
+ * <li>task is running if at least one job is running and none are failed or
+ * cancelled</li>
+ * <li>task is cancelled if at least one job is cancelled and none failed</li>
+ * <li>task is failed or server error if at least one job is failed or server
+ * error</li>
+ * </ul>
+ */
+ private void updateGlobalStatus()
+ {
+ int precedence = -1;
+ for (BaseJob job : jobs)
+ {
+ JobStatus status = job.getStatus();
+ int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status);
+ if (precedence < jobPrecedence)
+ precedence = jobPrecedence;
+ }
+ if (precedence >= 0)
+ {
+ setStatus(JobStatus.statusPrecedence[precedence]);
+ }
+ }
+
+ @Override
+ public void cancel()
+ {
+ setStatus(JobStatus.CANCELLED);
+ if (future != null)
+ future.cancel(false);
+ cancelJobs();
+ }
+
+ @Override
+ public List<? extends BaseJob> getSubJobs()
+ {
+ return jobs;
+ }
+
+ /**
+ * Collect and process input sequences for submission and return the list of
+ * jobs to be submitted.
+ *
+ * @return list of jobs to be submitted
+ * @throws ServiceInputInvalidException
+ * input is invalid and the task should not be started
+ */
+ protected abstract List<T> prepare() throws ServiceInputInvalidException;
+
+ /**
+ * Submit all valid jobs to the server and store their job handles.
+ *
+ * @throws IOException
+ * if server error occurred
+ */
+ protected void startJobs() throws IOException
+ {
+ for (BaseJob job : jobs)
+ {
+ if (job.isInputValid() && job.getStatus() == JobStatus.READY)
+ {
+ WebServiceJobHandle serverJob = client.submit(job.getInputSequences(),
+ args, credentials);
+ job.setServerJob(serverJob);
+ job.setStatus(JobStatus.SUBMITTED);
+ }
+ }
+ }
+
+ /**
+ * Poll all running jobs and update their status and logs. Polling is repeated
+ * periodically until this method return true when all jobs are done.
+ *
+ * @return {@code true] if all jobs are done @throws IOException if server
+ * error occurred
+ */
+ protected boolean pollJobs() throws IOException
+ {
+ boolean allDone = true;
+ for (BaseJob job : jobs)
+ {
+ if (job.isInputValid() && !job.getStatus().isDone())
+ {
+ WebServiceJobHandle serverJob = job.getServerJob();
+ job.setStatus(client.getStatus(serverJob));
+ job.setLog(client.getLog(serverJob));
+ job.setErrorLog(client.getErrorLog(serverJob));
+ }
+ allDone &= job.isCompleted();
+ }
+ return allDone;
+ }
+
+ /**
+ * Fetch and process the outputs produced by jobs and return the final result
+ * of the task. The method is called once all jobs have finished execution. If
+ * this method raises {@link IOException} it will be called again after a
+ * delay. All IO operations should happen before data processing, so
+ * potentially expensive computation is avoided in case of an error.
+ *
+ * @return final result of the computation
+ * @throws IOException
+ * if server error occurred
+ */
+ protected abstract R done() throws IOException;
+
+ /**
+ * Cancel all running jobs. Used in case of task failure to cleanup the
+ * resources or when the task has been cancelled.
+ */
+ protected void cancelJobs()
+ {
+ for (BaseJob job : jobs)
+ {
+ if (!job.isCompleted())
+ {
+ try
+ {
+ if (job.getServerJob() != null)
+ {
+ client.cancel(job.getServerJob());
+ }
+ job.setStatus(JobStatus.CANCELLED);
+ } catch (IOException e)
+ {
+ Cache.log.error(format("failed to cancel job %s", job.getServerJob()), e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public R getResult()
+ {
+ return result;
+ }
+
+ @Override
+ public String toString()
+ {
+ var status = taskStatus != null ? taskStatus.name() : "UNSET";
+ return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, status);
+ }
+}
--- /dev/null
+package jalview.ws2.actions;
+
+import java.util.EnumSet;
+import java.util.Objects;
+
+import jalview.ws2.actions.api.ActionI;
+import jalview.ws2.api.CredentialType;
+import jalview.ws2.api.WebService;
+
+/**
+ * An abstract base class storing common data and implementing their getters
+ * defined in {@link ActionI} interface. The concrete action implementations are
+ * encouraged to extend this class and provide their own {@code perform} and
+ * {@code isActive} implementations.
+ *
+ * @author mmwarowny
+ * @param <R>
+ * task result type
+ */
+public abstract class BaseAction<R> implements ActionI<R>
+{
+ public static abstract class Builder<A extends BaseAction<?>>
+ {
+ protected WebService<A> webService;
+
+ protected String name = null;
+
+ protected String tooltip = "";
+
+ protected String subcategory = null;
+
+ protected int minSequences = -1;
+
+ protected int maxSequences = -1;
+
+ protected boolean allowProtein = true;
+
+ protected boolean allowNucleotide = true;
+
+ protected EnumSet<CredentialType> requiredCredentials = EnumSet.noneOf(CredentialType.class);
+
+ public Builder()
+ {
+ }
+
+ public void name(String val)
+ {
+ this.name = val;
+ }
+
+ public void webService(WebService<A> val)
+ {
+ this.webService = val;
+ }
+
+ public void tooltip(String val)
+ {
+ tooltip = val;
+ }
+
+ public void subcategory(String val)
+ {
+ subcategory = val;
+ }
+
+ public void minSequences(int val)
+ {
+ minSequences = val;
+ }
+
+ public void maxSequecnes(int val)
+ {
+ maxSequences = val;
+ }
+
+ public void allowProtein(boolean val)
+ {
+ allowProtein = val;
+ }
+
+ public void allowNucleotide(boolean val)
+ {
+ allowNucleotide = val;
+ }
+
+ public void addRequiredCredential(CredentialType val)
+ {
+ requiredCredentials.add(val);
+ }
+
+ public void requiredCredentials(EnumSet<CredentialType> val)
+ {
+ requiredCredentials = val;
+ }
+ }
+
+ protected final WebService<? extends ActionI<R>> webService;
+
+ protected final String name;
+
+ protected final String tooltip;
+
+ protected final String subcategory;
+
+ protected final int minSequences;
+
+ protected final int maxSequences;
+
+ protected final boolean allowProtein;
+
+ protected final boolean allowNucleotide;
+
+ protected final EnumSet<CredentialType> requiredCredentials;
+
+ protected BaseAction(Builder<? extends BaseAction<R>> builder)
+ {
+ Objects.requireNonNull(builder.webService);
+ this.webService = builder.webService;
+ this.name = builder.name;
+ this.tooltip = builder.tooltip;
+ this.subcategory = builder.subcategory;
+ this.minSequences = builder.minSequences;
+ this.maxSequences = builder.maxSequences;
+ this.allowProtein = builder.allowProtein;
+ this.allowNucleotide = builder.allowNucleotide;
+ this.requiredCredentials = builder.requiredCredentials;
+ }
+
+ @Override
+ public WebService<? extends ActionI<R>> getWebService()
+ {
+ return webService;
+ }
+
+ @Override
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Returns a full name of the action which comprises of the service name and
+ * the action name if present.
+ *
+ * @return full name of this action
+ */
+ public String getFullName()
+ {
+ if (name == null || name.isEmpty())
+ return webService.getName();
+ else
+ return webService.getName() + " " + name;
+ }
+
+ @Override
+ public String getTooltip()
+ {
+ return tooltip;
+ }
+
+ @Override
+ public String getSubcategory()
+ {
+ return subcategory;
+ }
+
+ @Override
+ public int getMinSequences()
+ {
+ return minSequences;
+ }
+
+ @Override
+ public int getMaxSequences()
+ {
+ return maxSequences;
+ }
+
+ @Override
+ public boolean doAllowProtein()
+ {
+ return allowProtein;
+ }
+
+ @Override
+ public boolean doAllowNucleotide()
+ {
+ return allowNucleotide;
+ }
+
+ @Override
+ public EnumSet<CredentialType> getRequiredCredentials()
+ {
+ return requiredCredentials;
+ }
+}
--- /dev/null
+package jalview.ws2.actions;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.List;
+
+import jalview.datamodel.SequenceI;
+import jalview.util.MathUtils;
+import jalview.ws2.actions.api.JobI;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.api.WebServiceJobHandle;
+
+/**
+ * Basic implementation of the {@link JobI} interface which stores internal job
+ * id, status, log and error log and provides getters to those fields.
+ * Additionally, it stores sequences that will be submitted as job input and the
+ * handle to the job on the server. Extending classes can add extra fields in
+ * order to associate additional data with the job.
+ *
+ * Observers can be registered with this bean-like object to listen to changes
+ * to {@code status}, {@code log}, and {@code errorLog} properties. Typically,
+ * the events are delegated to the {@link TaskEventListener} objects observing
+ * the task that created this job.
+ *
+ * @author mmwarowny
+ */
+public abstract class BaseJob implements JobI
+{
+ protected final long internalId = MathUtils.getUID();
+
+ protected final List<SequenceI> inputSeqs;
+
+ protected JobStatus status = null;
+
+ protected String log = "";
+
+ protected String errorLog = "";
+
+ /* FIXME: server job is not specific to the BaseJob and should preferably
+ * be managed by classes using clients (tasks). */
+ protected WebServiceJobHandle serverJob;
+
+ public BaseJob(List<SequenceI> inputSeqs)
+ {
+ this.inputSeqs = inputSeqs;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final long getInternalId()
+ {
+ return internalId;
+ }
+
+ /**
+ * Return the list of input sequences associated with this job.
+ *
+ * @return input sequences
+ */
+ public List<SequenceI> getInputSequences()
+ {
+ return inputSeqs;
+ }
+
+ /**
+ * Check if inputs make a valid job.
+ *
+ * @return {@code true} if the input is valid.
+ */
+ public abstract boolean isInputValid();
+
+ /**
+ * Check if the job is completed, This includes jobs with invalid input,
+ * successful and unsuccessful termination.
+ *
+ * @return {@code true} if job is completed successfully or not
+ */
+ public boolean isCompleted()
+ {
+ return !isInputValid() || getStatus().isDone();
+ }
+
+ @Override
+ public final JobStatus getStatus()
+ {
+ return status;
+ }
+
+ /**
+ * Set new status of the job and notify listeners of the change. Job status is
+ * managed internally by tasks and should not be modified outside the task
+ * which created this job.
+ *
+ * @param status
+ * new job status
+ */
+ public final void setStatus(JobStatus status)
+ {
+ JobStatus oldStatus = this.status;
+ this.status = status;
+ pcs.firePropertyChange("status", oldStatus, status);
+ }
+
+ @Override
+ public final String getLog()
+ {
+ return log;
+ }
+
+ /**
+ * Set log text and notify listeners of the change. Log is managed by tasks
+ * which created the job and should not be modified by other classes.
+ *
+ * @param log
+ * new log
+ */
+ public final void setLog(String log)
+ {
+ String oldLog = this.log;
+ this.log = log;
+ pcs.firePropertyChange("log", oldLog, log);
+ }
+
+ @Override
+ public final String getErrorLog()
+ {
+ return errorLog;
+ }
+
+ /**
+ * Set error log text and notify listeners of the change. Error log is managed
+ * by tasks which created the job and should not be modified by other classes.
+ *
+ * @param errorLog
+ */
+ public final void setErrorLog(String errorLog)
+ {
+ String oldLog = this.errorLog;
+ this.errorLog = errorLog;
+ pcs.firePropertyChange("errorLog", oldLog, errorLog);
+ }
+
+ /**
+ * Return the job handle that identifies this job running on the server or
+ * {@code null} if the job was not submitted.
+ *
+ * @return server job handle
+ */
+ public final WebServiceJobHandle getServerJob()
+ {
+ return serverJob;
+ }
+
+ /**
+ * Set the server job handle once the job was submitted to the server. The
+ * handler is managed by the task which created this job and should not be
+ * modified by other classes.
+ *
+ * @param job
+ */
+ public final void setServerJob(WebServiceJobHandle job)
+ {
+ this.serverJob = job;
+ }
+
+ private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
+
+ /**
+ * Register an observer that will be notified of changes to status, log and
+ * error log.
+ *
+ * @param listener
+ * property change listener
+ */
+ public final void addPropertyChagneListener(PropertyChangeListener listener)
+ {
+ pcs.addPropertyChangeListener(listener);
+ }
+
+ /**
+ * Remove the property listener from this object.
+ *
+ * @param listener
+ * listener to remove
+ */
+ public final void removePropertyChangeListener(PropertyChangeListener listener)
+ {
+ pcs.removePropertyChangeListener(listener);
+ }
+}
--- /dev/null
+package jalview.ws2.actions;
+
+/**
+ * An exception thrown to indicate that the input is invalid and the service
+ * cannot be started.
+ *
+ * @author mmwarowny
+ *
+ */
+public class ServiceInputInvalidException extends Exception
+{
+ /**
+ *
+ */
+ private static final long serialVersionUID = 174066679057181584L;
+
+ public ServiceInputInvalidException(String message)
+ {
+ super(message);
+ }
+
+ public ServiceInputInvalidException(Throwable cause)
+ {
+ super(cause);
+ }
+
+ public ServiceInputInvalidException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
--- /dev/null
+package jalview.ws2.actions.alignment;
+
+import java.util.List;
+import java.util.Objects;
+
+import jalview.viewmodel.AlignmentViewport;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.actions.BaseAction;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.client.api.AlignmentWebServiceClientI;
+
+/**
+ * Implementation of the {@link BaseAction} that runs alignment services. This
+ * type of action requires {@link AlignmentWebServiceClientI} to retrieve
+ * alignment result from the server.
+ *
+ * @author mmwarowny
+ *
+ */
+public class AlignmentAction extends BaseAction<AlignmentResult>
+{
+ /**
+ * A builder for AlignemntActions. Adds {@code client} and {@code submitGaps}
+ * parameters to the base builder.
+ *
+ * @author mmwarowny
+ */
+ public static class Builder extends BaseAction.Builder<AlignmentAction>
+ {
+ protected AlignmentWebServiceClientI client;
+
+ protected boolean submitGaps = false;
+
+ public Builder(AlignmentWebServiceClientI client)
+ {
+ super();
+ Objects.requireNonNull(client);
+ this.client = client;
+ }
+
+ public void submitGaps(boolean val)
+ {
+ submitGaps = val;
+ }
+
+ public AlignmentAction build()
+ {
+ return new AlignmentAction(this);
+ }
+ }
+
+ public static Builder newBuilder(AlignmentWebServiceClientI client)
+ {
+ return new Builder(client);
+ }
+
+ protected final boolean submitGaps;
+
+ protected final AlignmentWebServiceClientI client;
+
+ public AlignmentAction(Builder builder)
+ {
+ super(builder);
+ submitGaps = builder.submitGaps;
+ client = builder.client;
+ }
+
+ @Override
+ public TaskI<AlignmentResult> perform(AlignmentViewport viewport,
+ List<ArgumentI> args, Credentials credentials,
+ TaskEventListener<AlignmentResult> handler)
+ {
+ var msa = viewport.getAlignmentView(true);
+ var task = new AlignmentTask(
+ client, this, args, credentials, msa, viewport, submitGaps, handler);
+ task.start(viewport.getServiceExecutor());
+ return task;
+ }
+
+ /**
+ * Returns if the action is active for the given viewport. Alignment services
+ * are non-interactive, so the action is never active.
+ */
+ @Override
+ public boolean isActive(AlignmentViewport viewport)
+ {
+ return false;
+ }
+
+}
--- /dev/null
+package jalview.ws2.actions.alignment;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import jalview.analysis.AlignSeq;
+import jalview.analysis.SeqsetUtils;
+import jalview.analysis.SeqsetUtils.SequenceInfo;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.util.Comparison;
+import jalview.ws2.actions.BaseJob;
+import jalview.ws2.api.WebServiceJobHandle;
+
+/**
+ * A wrapper class that extends basic job container with data specific to
+ * alignment services. It stores input and empty sequences (with uniquified
+ * names) along the original sequence information. {@link AlignmentJob} objects
+ * are created by {@link AlignmentTask} during a preparation stage.
+ *
+ * @author mmwarowny
+ *
+ */
+class AlignmentJob extends BaseJob
+{
+ private final List<SequenceI> emptySeqs;
+
+ private final Map<String, SequenceInfo> names;
+
+ private AlignmentI alignmentResult;
+
+ AlignmentJob(List<SequenceI> inputSeqs, List<SequenceI> emptySeqs,
+ Map<String, SequenceInfo> names)
+ {
+ super(Collections.unmodifiableList(inputSeqs));
+ this.emptySeqs = Collections.unmodifiableList(emptySeqs);
+ this.names = Collections.unmodifiableMap(names);
+ }
+
+ public static AlignmentJob create(SequenceI[] seqs, int minlen, boolean keepGaps)
+ {
+ int nseqs = 0;
+ for (int i = 0; i < seqs.length; i++)
+ {
+ if (seqs[i].getEnd() - seqs[i].getStart() >= minlen)
+ nseqs++;
+ }
+ boolean valid = nseqs > 1; // need at least two sequences
+ Map<String, SequenceInfo> names = new LinkedHashMap<>();
+ List<SequenceI> inputSeqs = new ArrayList<>();
+ List<SequenceI> emptySeqs = new ArrayList<>();
+ for (int i = 0; i < seqs.length; i++)
+ {
+ SequenceI seq = seqs[i];
+ String newName = SeqsetUtils.unique_name(i);
+ names.put(newName, SeqsetUtils.SeqCharacterHash(seq));
+ if (valid && seq.getEnd() - seq.getStart() >= minlen)
+ {
+ // make new input sequence
+ String seqString = seq.getSequenceAsString();
+ if (!keepGaps)
+ seqString = AlignSeq.extractGaps(Comparison.GapChars, seqString);
+ inputSeqs.add(new Sequence(newName, seqString));
+ }
+ else
+ {
+ String seqString = "";
+ if (seq.getEnd() >= seq.getStart()) // true if gaps only
+ {
+ seqString = seq.getSequenceAsString();
+ if (!keepGaps)
+ seqString = AlignSeq.extractGaps(Comparison.GapChars, seqString);
+ }
+ emptySeqs.add(new Sequence(newName, seqString));
+ }
+ }
+ return new AlignmentJob(inputSeqs, emptySeqs, names);
+ }
+
+ @Override
+ public boolean isInputValid()
+ {
+ return inputSeqs.size() >= 2;
+ }
+
+ List<SequenceI> getEmptySequences()
+ {
+ return emptySeqs;
+ }
+
+ Map<String, SequenceInfo> getNames()
+ {
+ return names;
+ }
+
+ boolean hasResult()
+ {
+ return alignmentResult != null;
+ }
+
+ AlignmentI getAlignmentResult()
+ {
+ return alignmentResult;
+ }
+
+ void setAlignmentResult(AlignmentI alignment)
+ {
+ this.alignmentResult = alignment;
+ }
+}
--- /dev/null
+package jalview.ws2.actions.alignment;
+
+import java.io.IOException;
+
+import jalview.datamodel.AlignmentI;
+
+import jalview.ws2.api.WebServiceJobHandle;
+import jalview.ws2.client.api.AlignmentWebServiceClientI;
+import jalview.ws2.client.api.WebServiceClientI;
+
+/**
+ * An interface for providing alignment results to the alignment services. Web
+ * service clients that want to support alignment actions must implement this
+ * interface in addition to {@link WebServiceClientI}.
+ *
+ * @author mmwarowny
+ *
+ * @see AlignmentWebServiceClientI
+ */
+public interface AlignmentProviderI
+{
+ /**
+ * Get the alignment result for the job from the server.
+ *
+ * @param job
+ * web service job
+ * @return alignment result
+ * @throws IOException
+ * server communication error
+ */
+ public AlignmentI getAlignment(WebServiceJobHandle job) throws IOException;
+}
--- /dev/null
+package jalview.ws2.actions.alignment;
+
+import java.util.List;
+
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AlignmentOrder;
+import jalview.datamodel.HiddenColumns;
+import jalview.ws2.actions.api.TaskEventListener;
+
+/**
+ * A data container storing the output of multiple sequence alignment services.
+ * The object is constructed by an {@link AlignmentTask} on completion and
+ * passed to the handler {@link TaskEventListener#taskCompleted(TaskI, Object)}
+ * method as a result.
+ *
+ * @author mmwarowny
+ */
+public class AlignmentResult
+{
+ final AlignmentI aln;
+
+ final List<AlignmentOrder> alorders;
+
+ final HiddenColumns hidden;
+
+ AlignmentResult(AlignmentI aln, List<AlignmentOrder> alorders,
+ HiddenColumns hidden)
+ {
+ this.aln = aln;
+ this.alorders = alorders;
+ this.hidden = hidden;
+ }
+
+ public AlignmentI getAlignment()
+ {
+ return aln;
+ }
+
+ public List<AlignmentOrder> getAlignmentOrders()
+ {
+ return alorders;
+ }
+
+ public HiddenColumns getHiddenColumns()
+ {
+ return hidden;
+ }
+}
--- /dev/null
+package jalview.ws2.actions.alignment;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jalview.analysis.AlignmentSorter;
+import jalview.analysis.SeqsetUtils;
+import jalview.analysis.SeqsetUtils.SequenceInfo;
+import jalview.api.AlignViewportI;
+import jalview.bin.Cache;
+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.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.actions.AbstractPollableTask;
+import jalview.ws2.actions.ServiceInputInvalidException;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.client.api.AlignmentWebServiceClientI;
+
+/**
+ * Implementation of an abstract pollable task used by alignment service
+ * actions.
+ *
+ * @author mmwarowny
+ *
+ */
+class AlignmentTask extends AbstractPollableTask<AlignmentJob, AlignmentResult>
+{
+ /* task parameters set in the constructor */
+ private final AlignmentWebServiceClientI client;
+
+ private final AlignmentAction action;
+
+ private final AlignmentView msa; // a.k.a. input
+
+ private final AlignViewportI viewport;
+
+ private final boolean submitGaps;
+
+ private final AlignmentI currentView;
+
+ private final AlignmentI dataset;
+
+ private final char gapChar;
+
+ private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
+
+ AlignmentTask(AlignmentWebServiceClientI client, AlignmentAction action,
+ List<ArgumentI> args, Credentials credentials,
+ AlignmentView msa, AlignViewportI viewport, boolean submitGaps,
+ TaskEventListener<AlignmentResult> eventListener)
+ {
+ super(client, args, credentials, eventListener);
+ this.client = client;
+ this.action = action;
+ this.msa = msa;
+ this.viewport = viewport;
+ this.submitGaps = submitGaps;
+ this.currentView = viewport.getAlignment();
+ this.dataset = viewport.getAlignment().getDataset();
+ this.gapChar = viewport.getGapCharacter();
+ List<AlignedCodonFrame> cf = viewport.getAlignment().getCodonFrames();
+ if (cf != null)
+ this.codonFrame.addAll(cf);
+ }
+
+ @Override
+ protected List<AlignmentJob> prepare() throws ServiceInputInvalidException
+ {
+ Cache.log.info(format("starting alignment service %s:%s",
+ client.getClientName(), action.getName()));
+ SequenceI[][] conmsa = msa.getVisibleContigs(gapChar);
+ if (conmsa == null)
+ {
+ throw new ServiceInputInvalidException("no visible contigs for alignment");
+ }
+ List<AlignmentJob> jobs = new ArrayList<>(conmsa.length);
+ boolean validInput = false;
+ for (int i = 0; i < conmsa.length; i++)
+ {
+ AlignmentJob job = AlignmentJob.create(conmsa[i], 2, submitGaps);
+ validInput |= job.isInputValid(); // at least one input is valid
+ job.setStatus(job.isInputValid() ? JobStatus.READY : JobStatus.INVALID);
+ jobs.add(job);
+ }
+ this.jobs = jobs;
+ if (!validInput)
+ {
+ throw new ServiceInputInvalidException("no valid sequences for alignment");
+ }
+ return jobs;
+ }
+
+ @Override
+ protected AlignmentResult done() throws IOException
+ {
+ IOException lastIOE = null;
+ for (AlignmentJob job : jobs)
+ {
+ if (job.isInputValid() && job.getStatus() == JobStatus.COMPLETED &&
+ !job.hasResult())
+ {
+ try
+ {
+ job.setAlignmentResult(client.getAlignment(job.getServerJob()));
+ } catch (IOException e)
+ {
+ lastIOE = e;
+ }
+ }
+ }
+ if (lastIOE != null)
+ throw lastIOE; // do not proceed unless all results has been retrieved
+
+ List<AlignmentOrder> alorders = new ArrayList<>();
+ SequenceI[][] results = new SequenceI[jobs.size()][];
+ AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
+ for (int i = 0; i < jobs.size(); i++)
+ {
+ /* alternative implementation of MsaWSJob#getAlignment */
+ AlignmentJob job = jobs.get(i);
+ if (!job.hasResult())
+ continue;
+ AlignmentI alignment = job.getAlignmentResult();
+ int alnSize = alignment.getSequences().size();
+ char gapChar = alnSize > 0 ? alignment.getGapCharacter() : '-';
+ List<SequenceI> emptySeqs = job.getEmptySequences();
+ List<SequenceI> alnSeqs = new ArrayList<>(alnSize);
+ // create copies of all sequences involved
+ for (SequenceI seq : alignment.getSequences())
+ {
+ alnSeqs.add(new Sequence(seq));
+ }
+ for (SequenceI seq : emptySeqs)
+ {
+ alnSeqs.add(new Sequence(seq));
+ }
+ // find the width of the longest sequence
+ int width = 0;
+ for (var seq: alnSeqs)
+ width = Integer.max(width, seq.getLength());
+ // make a sequence of gaps only to cut/paste
+ String gapSeq;
+ {
+ char[] gaps = new char[width];
+ Arrays.fill(gaps, gapChar);
+ gapSeq = new String(gaps);
+ }
+ for (var seq: alnSeqs)
+ {
+ if (seq.getLength() < width)
+ {
+ // pad sequences shorter than the target width with gaps
+ seq.setSequence(seq.getSequenceAsString()
+ + gapSeq.substring(seq.getLength()));
+ }
+ }
+ SequenceI[] result = alnSeqs.toArray(new SequenceI[0]);
+ AlignmentOrder msaOrder = new AlignmentOrder(result);
+ AlignmentSorter.recoverOrder(result);
+ Map<String, SequenceInfo> names = new HashMap<>(job.getNames());
+ SeqsetUtils.deuniquify(names, result);
+
+ alorders.add(msaOrder);
+ results[i] = result;
+ orders[i] = msaOrder;
+ }
+ Object[] newView = msa.getUpdatedView(results, orders, gapChar);
+ // 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", action.getName());
+ if (dataset != null)
+ aln.setDataset(dataset);
+
+ propagateDatasetMappings(aln);
+ return new AlignmentResult(aln, alorders, hidden);
+ }
+
+ /**
+ * Conserve dataset references to sequence objects returned from web services.
+ * Propagate AlignedCodonFrame data from {@code codonFrame} to {@code aln}.
+ * TODO: Refactor to datamodel
+ */
+ private void propagateDatasetMappings(AlignmentI aln)
+ {
+ if (codonFrame != null)
+ {
+ SequenceI[] alignment = aln.getSequencesArray();
+ for (final SequenceI seq : alignment)
+ {
+ for (AlignedCodonFrame acf : codonFrame)
+ {
+ if (acf != null && acf.involvesSequence(seq))
+ {
+ aln.addCodonFrame(acf);
+ break;
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+package jalview.ws2.actions.annotation;
+
+import java.util.List;
+import java.util.Objects;
+
+import jalview.viewmodel.AlignmentViewport;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.actions.BaseAction;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.client.api.AnnotationWebServiceClientI;
+
+public class AnnotationAction extends BaseAction<AnnotationResult>
+{
+ /**
+ * A builder of {@link AnnotationAction} instances.
+ */
+ public static class Builder extends BaseAction.Builder<AnnotationAction>
+ {
+ protected AnnotationWebServiceClientI client;
+
+ protected boolean alignmentAnalysis = false;
+
+ protected boolean requireAlignedSequences = false;
+
+ protected boolean filterSymbols = true;
+
+ public Builder(AnnotationWebServiceClientI client)
+ {
+ super();
+ Objects.requireNonNull(client);
+ this.client = client;
+ }
+
+ /**
+ * Set if action is an alignment analysis action.
+ */
+ public void alignmentAnalysis(boolean val)
+ {
+ alignmentAnalysis = val;
+ }
+
+ /**
+ * Set if action require aligned sequences.
+ */
+ public void requireAlignedSequences(boolean val)
+ {
+ requireAlignedSequences = val;
+ }
+
+ /**
+ * Set if action requires non-standard residues to be filtered out
+ */
+ public void filterSymbols(boolean val)
+ {
+ filterSymbols = val;
+ }
+
+ public AnnotationAction build()
+ {
+ return new AnnotationAction(this);
+ }
+ }
+
+ public static Builder newBuilder(AnnotationWebServiceClientI client)
+ {
+ return new Builder(client);
+ }
+
+ protected final AnnotationWebServiceClientI client;
+
+ protected final boolean alignmentAnalysis;
+
+ protected final boolean requireAlignedSequences;
+
+ protected final boolean filterSymbols;
+
+ protected AnnotationAction(Builder builder)
+ {
+ super(builder);
+ client = builder.client;
+ alignmentAnalysis = builder.alignmentAnalysis;
+ requireAlignedSequences = builder.requireAlignedSequences;
+ filterSymbols = builder.filterSymbols;
+ }
+
+ @Override
+ public TaskI<AnnotationResult> perform(AlignmentViewport viewport,
+ List<ArgumentI> args, Credentials credentials,
+ TaskEventListener<AnnotationResult> handler)
+ {
+ var task = new AnnotationTask(client, this, args, credentials, viewport,
+ handler);
+ task.start(viewport.getCalcManager());
+ return task;
+ }
+
+ /**
+ * Return if this action is an alignment analysis service.
+ */
+ public boolean isAlignmentAnalysis()
+ {
+ return alignmentAnalysis;
+ }
+
+ /**
+ * Return if this action require sequences to be aligned.
+ */
+ public boolean getRequireAlignedSequences()
+ {
+ return requireAlignedSequences;
+ }
+
+ /**
+ * Return if this action require non-standard symbols to be filtered out.
+ */
+ public boolean getFilterSymbols()
+ {
+ return filterSymbols;
+ }
+
+ @Override
+ public boolean isActive(AlignmentViewport viewport)
+ {
+ return false;
+ }
+}
--- /dev/null
+package jalview.ws2.actions.annotation;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jalview.analysis.AlignSeq;
+import jalview.analysis.SeqsetUtils;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.schemes.ResidueProperties;
+import jalview.util.Comparison;
+import jalview.ws2.actions.BaseJob;
+
+public class AnnotationJob extends BaseJob
+{
+ final boolean[] gapMap;
+
+ final Map<String, SequenceI> seqNames;
+
+ final int start, end;
+
+ final int minSize;
+
+ List<AlignmentAnnotation> returnedAnnotations = Collections.emptyList();
+
+ Map<String, FeatureColourI> featureColours = Collections.emptyMap();
+
+ Map<String, FeatureMatcherSetI> featureFilters = Collections.emptyMap();
+
+
+ public AnnotationJob(List<SequenceI> inputSeqs, boolean[] gapMap,
+ Map<String, SequenceI> seqNames, int start, int end, int minSize)
+ {
+ super(inputSeqs);
+ this.gapMap = gapMap;
+ this.seqNames = seqNames;
+ this.start = start;
+ this.end = end;
+ this.minSize = minSize;
+ }
+
+ @Override
+ public boolean isInputValid()
+ {
+ int nvalid = 0;
+ for (SequenceI sq : getInputSequences())
+ if (sq.getStart() <= sq.getEnd())
+ nvalid++;
+ return nvalid >= minSize;
+ }
+
+ public static AnnotationJob create(AnnotatedCollectionI inputSeqs,
+ boolean bySequence, boolean submitGaps, boolean requireAligned,
+ boolean filterNonStandardResidues, int minSize)
+ {
+ List<SequenceI> seqs = new ArrayList<>();
+ int minlen = 10;
+ int ln = -1;
+ Map<String, SequenceI> seqNames = bySequence ? new HashMap<>() : null;
+ BitSet gapMap = new BitSet();
+ int gapMapSize = 0;
+ int start = inputSeqs.getStartRes();
+ int 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;
+ if (bySequence)
+ 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() + 1);
+ if (seqNames != null)
+ seqNames.put(newName, sq);
+ Sequence seq;
+ if (submitGaps)
+ {
+ seq = new Sequence(newName, sq.getSequenceAsString());
+ gapMapSize = Math.max(gapMapSize, seq.getLength());
+ for (int pos : sq.gapMap())
+ {
+ char sqchr = sq.getCharAt(pos);
+ boolean include = !filterNonStandardResidues;
+ include |= sq.isProtein() ? ResidueProperties.aaIndex[sqchr] < 20
+ : ResidueProperties.nucleotideIndex[sqchr] < 5;
+ if (include)
+ gapMap.set(pos);
+ }
+ }
+ else
+ {
+ // TODO: add ability to exclude hidden regions
+ seq = new Sequence(newName, AlignSeq.extractGaps(Comparison.GapChars,
+ sq.getSequenceAsString(start, end + 1)));
+ // for annotation need to also record map to sequence start/end
+ // position in range
+ // then transfer back to original sequence on return.
+ }
+ seqs.add(seq);
+ ln = Math.max(ln, seq.getLength());
+ }
+ }
+
+ if (requireAligned && submitGaps)
+ {
+ int realWidth = gapMap.cardinality();
+ for (int i = 0; i < seqs.size(); i++)
+ {
+ SequenceI sq = seqs.get(i);
+ char[] padded = new char[realWidth];
+ char[] original = sq.getSequence();
+ for (int op = 0, pp = 0; pp < realWidth; op++)
+ {
+ if (gapMap.get(op))
+ {
+ if (original.length > op)
+ padded[pp++] = original[op];
+ else
+ padded[pp++] = '-';
+ }
+ }
+ seqs.set(i, new Sequence(sq.getName(), padded));
+ }
+ }
+ boolean[] gapMapArray = new boolean[gapMapSize];
+ for (int i = 0; i < gapMapSize; i++)
+ gapMapArray[i] = gapMap.get(i);
+ return new AnnotationJob(seqs, gapMapArray, seqNames, start, end, minSize);
+ }
+}
--- /dev/null
+package jalview.ws2.actions.annotation;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import jalview.api.FeatureColourI;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.ws2.api.WebServiceJobHandle;
+import jalview.ws2.client.api.AnnotationWebServiceClientI;
+import jalview.ws2.client.api.WebServiceClientI;
+
+/**
+ * An interface for providing annotation results to the annotation services. It
+ * declares a method to attach annotations fetched from the server to sequences.
+ * Web service clients wanting to support annotation acitons must implement this
+ * interface in addition to {@link WebServiceClientI}
+ *
+ * @author mmwarowny
+ *
+ * @see AnnotationWebServiceClientI
+ */
+public interface AnnotationProviderI
+{
+ /**
+ * Retrieves annotations from the job result on the server and attaches them
+ * to provided sequences. Additionally, adds feature colours and filters to
+ * provided containers.
+ *
+ * @param job
+ * web service job
+ * @param sequences
+ * sequences the annotations will be added to
+ * @param colours
+ * container for feature colours
+ * @param filters
+ * container for feature filters
+ * @return sequence and alignment annotation rows that should be made
+ * visible/updated on alignment
+ * @throws IOException
+ * annotation retrieval failed
+ */
+ public List<AlignmentAnnotation> attachAnnotations(WebServiceJobHandle job,
+ List<SequenceI> sequences, Map<String, FeatureColourI> colours,
+ Map<String, FeatureMatcherSetI> filters) throws IOException;
+}
--- /dev/null
+package jalview.ws2.actions.annotation;
+
+import java.util.List;
+import java.util.Map;
+
+import jalview.api.FeatureColourI;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.features.FeatureMatcherSetI;
+
+/**
+ * A simple data container storing the output of annotation tasks. The object is
+ * constructed on {@link AnnotationTask} completion and passed to an appropriate
+ * handler.
+ *
+ * @author mmwarowny
+ *
+ */
+public class AnnotationResult
+{
+ final List<AlignmentAnnotation> annotations;
+
+ final boolean transferFeatures;
+
+ final Map<String, FeatureColourI> featureColours;
+
+ final Map<String, FeatureMatcherSetI> featureFilters;
+
+ public AnnotationResult(List<AlignmentAnnotation> annotations, boolean transferFeatures,
+ Map<String, FeatureColourI> featureColours, Map<String, FeatureMatcherSetI> featureFilters)
+ {
+ this.annotations = annotations;
+ this.transferFeatures = transferFeatures;
+ this.featureColours = featureColours;
+ this.featureFilters = featureFilters;
+ }
+
+ public List<AlignmentAnnotation> getAnnotations()
+ {
+ return annotations;
+ }
+
+ public boolean getTransferFeatures()
+ {
+ return transferFeatures;
+ }
+
+ public Map<String, FeatureColourI> getFeatureColours()
+ {
+ return featureColours;
+ }
+
+ public Map<String, FeatureMatcherSetI> getFeatureFilters()
+ {
+ return featureFilters;
+ }
+}
--- /dev/null
+package jalview.ws2.actions.annotation;
+
+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 jalview.analysis.AlignmentAnnotationUtils;
+import jalview.api.AlignCalcManagerI2;
+import jalview.api.AlignCalcWorkerI;
+import jalview.api.AlignViewportI;
+import jalview.api.FeatureColourI;
+import jalview.api.PollableAlignCalcWorkerI;
+import jalview.bin.Cache;
+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.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.schemes.FeatureSettingsAdapter;
+import jalview.util.ArrayUtils;
+import jalview.util.MapList;
+import jalview.util.MathUtils;
+import jalview.util.Pair;
+import jalview.workers.AlignCalcWorker;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.actions.AbstractPollableTask;
+import jalview.ws2.actions.BaseJob;
+import jalview.ws2.actions.ServiceInputInvalidException;
+import jalview.ws2.actions.api.JobI;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.api.WebServiceJobHandle;
+import jalview.ws2.client.api.AnnotationWebServiceClientI;
+import jalview.ws2.helpers.DelegateJobEventListener;
+import jalview.ws2.helpers.TaskEventSupport;
+
+import static java.util.Objects.requireNonNullElse;
+
+public class AnnotationTask implements TaskI<AnnotationResult>
+{
+ private final long uid = MathUtils.getUID();
+
+ private AnnotationWebServiceClientI client;
+
+ private final AnnotationAction action;
+
+ private final List<ArgumentI> args;
+
+ private final Credentials credentials;
+
+ private final AlignViewportI viewport;
+
+ private final TaskEventSupport<AnnotationResult> eventHandler;
+
+ private JobStatus taskStatus = null;
+
+ private AlignCalcWorkerAdapter worker = null;
+
+ private List<AnnotationJob> jobs = Collections.emptyList();
+
+ private AnnotationResult result = null;
+
+ private DelegateJobEventListener<AnnotationResult> jobEventHandler;
+
+ private class AlignCalcWorkerAdapter extends AlignCalcWorker
+ implements PollableAlignCalcWorkerI
+ {
+ private boolean restarting = false;
+
+ AlignCalcWorkerAdapter(AlignCalcManagerI2 calcMan)
+ {
+ super(viewport, null);
+ this.calcMan = calcMan;
+ }
+
+ String getServiceName()
+ {
+ return action.getWebService().getName();
+ }
+
+ @Override
+ public void startUp() throws Throwable
+ {
+ if (alignViewport.isClosed())
+ {
+ stop();
+ throw new IllegalStateException("Starting annotation for closed viewport");
+ }
+ if (restarting)
+ eventHandler.fireTaskRestarted();
+ else
+ restarting = true;
+ jobs = Collections.emptyList();
+ try
+ {
+ jobs = prepare();
+ } catch (ServiceInputInvalidException e)
+ {
+ setStatus(JobStatus.INVALID);
+ eventHandler.fireTaskException(e);
+ throw e;
+ }
+ setStatus(JobStatus.READY);
+ eventHandler.fireTaskStarted(jobs);
+ for (var job : jobs)
+ {
+ job.addPropertyChagneListener(jobEventHandler);
+ }
+ try
+ {
+ startJobs();
+ } catch (IOException e)
+ {
+ eventHandler.fireTaskException(e);
+ cancelJobs();
+ setStatus(JobStatus.SERVER_ERROR);
+ throw e;
+ }
+ setStatus(JobStatus.SUBMITTED);
+ }
+
+ @Override
+ public boolean poll() throws Throwable
+ {
+ boolean done = AnnotationTask.this.poll();
+ updateGlobalStatus();
+ if (done)
+ {
+ retrieveAndProcessResult();
+ eventHandler.fireTaskCompleted(result);
+ }
+ return done;
+ }
+
+ private void retrieveAndProcessResult() throws IOException
+ {
+ result = retrieveResult();
+ updateOurAnnots(result.annotations);
+ if (result.transferFeatures)
+ {
+ final var featureColours = result.featureColours;
+ final var featureFilters = result.featureFilters;
+ 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);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void updateAnnotation()
+ {
+ var job = jobs.size() > 0 ? jobs.get(0) : null;
+ if (!calcMan.isWorking(this) && job != null)
+ {
+ var ret = updateResultAnnotation(job, job.returnedAnnotations);
+ updateOurAnnots(ret.get0());
+ }
+ }
+
+ private void updateOurAnnots(List<AlignmentAnnotation> newAnnots)
+ {
+ List<AlignmentAnnotation> oldAnnots = requireNonNullElse(ourAnnots, Collections.emptyList());
+ ourAnnots = newAnnots;
+ AlignmentI alignment = viewport.getAlignment();
+ for (AlignmentAnnotation an : oldAnnots)
+ {
+ if (!newAnnots.contains(an))
+ {
+ alignment.deleteAnnotation(an);
+ }
+ }
+ oldAnnots.clear();
+ for (AlignmentAnnotation an : ourAnnots)
+ {
+ viewport.getAlignment().validateAnnotation(an);
+ }
+ }
+
+ @Override
+ public void cancel()
+ {
+ cancelJobs();
+ }
+
+ void stop()
+ {
+ calcMan.disableWorker(this);
+ super.abortAndDestroy();
+ }
+
+ @Override
+ public void done()
+ {
+ for (var job : jobs)
+ {
+ if (job.isInputValid() && !job.isCompleted())
+ {
+ /* if done was called but job is not completed then it
+ * must have been stopped by an exception */
+ job.setStatus(JobStatus.SERVER_ERROR);
+ }
+ }
+ updateGlobalStatus();
+ // dispose of unfinished jobs just in case
+ cancelJobs();
+ }
+
+ @Override
+ public String toString()
+ {
+ return AnnotationTask.this.toString() + "$AlignCalcWorker@"
+ + Integer.toHexString(hashCode());
+ }
+ }
+
+ public AnnotationTask(AnnotationWebServiceClientI client,
+ AnnotationAction action, List<ArgumentI> args, Credentials credentials,
+ AlignViewportI viewport,
+ TaskEventListener<AnnotationResult> eventListener)
+ {
+ this.client = client;
+ this.action = action;
+ this.args = args;
+ this.credentials = credentials;
+ this.viewport = viewport;
+ this.eventHandler = new TaskEventSupport<>(this, eventListener);
+ this.jobEventHandler = new DelegateJobEventListener<>(this.eventHandler);
+ }
+
+ @Override
+ public long getUid()
+ {
+ return uid;
+ }
+
+ public void start(AlignCalcManagerI2 calcManager)
+ {
+ if (this.worker != null)
+ throw new IllegalStateException("task already started");
+ this.worker = new AlignCalcWorkerAdapter(calcManager);
+ if (taskStatus != JobStatus.CANCELLED)
+ {
+ List<AlignCalcWorkerI> oldWorkers = calcManager.getWorkersOfClass(
+ AlignCalcWorkerAdapter.class);
+ for (var worker : oldWorkers)
+ {
+ if (action.getWebService().getName().equalsIgnoreCase(
+ ((AlignCalcWorkerAdapter) worker).getServiceName()))
+ {
+ // remove interactive workers for the same service.
+ calcManager.removeWorker(worker);
+ calcManager.cancelWorker(worker);
+ }
+ }
+ if (action.getWebService().isInteractive())
+ calcManager.registerWorker(worker);
+ else
+ calcManager.startWorker(worker);
+ }
+ }
+
+ /*
+ * The following methods are mostly copied from the {@link AbstractPollableTask}
+ * TODO: move common functionality to a base class
+ */
+ @Override
+ public JobStatus getStatus()
+ {
+ return taskStatus;
+ }
+
+ private void setStatus(JobStatus status)
+ {
+ if (this.taskStatus != status)
+ {
+ Cache.log.debug(String.format("%s status change to %s", this, status.name()));
+ this.taskStatus = status;
+ eventHandler.fireTaskStatusChanged(status);
+ }
+ }
+
+ private void updateGlobalStatus()
+ {
+ int precedence = -1;
+ for (BaseJob job : jobs)
+ {
+ JobStatus status = job.getStatus();
+ int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status);
+ if (precedence < jobPrecedence)
+ precedence = jobPrecedence;
+ }
+ if (precedence >= 0)
+ {
+ setStatus(JobStatus.statusPrecedence[precedence]);
+ }
+ }
+
+ @Override
+ public List<? extends JobI> getSubJobs()
+ {
+ return jobs;
+ }
+
+ /**
+ * Create and return a list of annotation jobs from the current state of the
+ * viewport. Returned job are not started by this method and should be stored
+ * in a field and started separately.
+ *
+ * @return list of annotation jobs
+ * @throws ServiceInputInvalidException
+ * input data is not valid
+ */
+ private List<AnnotationJob> prepare() throws ServiceInputInvalidException
+ {
+ AlignmentI alignment = viewport.getAlignment();
+ if (alignment == null || alignment.getWidth() <= 0 ||
+ alignment.getSequences() == null)
+ throw new ServiceInputInvalidException("Alignment does not contain sequences");
+ if (alignment.isNucleotide() && !action.doAllowNucleotide())
+ throw new ServiceInputInvalidException(
+ action.getFullName() + " does not allow nucleotide sequences");
+ if (!alignment.isNucleotide() && !action.doAllowProtein())
+ throw new ServiceInputInvalidException(
+ action.getFullName() + " does not allow protein sequences");
+ boolean bySequence = !action.isAlignmentAnalysis();
+ AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null;
+ if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
+ inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
+ inputSeqs = alignment;
+ boolean submitGaps = action.isAlignmentAnalysis();
+ boolean requireAligned = action.getRequireAlignedSequences();
+ boolean filterSymbols = action.getFilterSymbols();
+ int minSize = action.getMinSequences();
+ AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
+ submitGaps, requireAligned, filterSymbols, minSize);
+ if (!job.isInputValid())
+ {
+ job.setStatus(JobStatus.INVALID);
+ throw new ServiceInputInvalidException("Annotation job has invalid input");
+ }
+ job.setStatus(JobStatus.READY);
+ return List.of(job);
+ }
+
+ private void startJobs() throws IOException
+ {
+ for (BaseJob job : jobs)
+ {
+ if (job.isInputValid() && job.getStatus() == JobStatus.READY)
+ {
+ var serverJob = client.submit(job.getInputSequences(),
+ args, credentials);
+ job.setServerJob(serverJob);
+ job.setStatus(JobStatus.SUBMITTED);
+ }
+ }
+ }
+
+ private boolean poll() throws IOException
+ {
+ boolean allDone = true;
+ for (BaseJob job : jobs)
+ {
+ if (job.isInputValid() && !job.getStatus().isDone())
+ {
+ WebServiceJobHandle serverJob = job.getServerJob();
+ job.setStatus(client.getStatus(serverJob));
+ job.setLog(client.getLog(serverJob));
+ job.setErrorLog(client.getErrorLog(serverJob));
+ }
+ allDone &= job.isCompleted();
+ }
+ return allDone;
+ }
+
+ private AnnotationResult retrieveResult() throws IOException
+ {
+ final Map<String, FeatureColourI> featureColours = new HashMap<>();
+ final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
+ var job = jobs.get(0);
+ List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
+ job.getServerJob(), job.getInputSequences(), featureColours,
+ featureFilters);
+ /* TODO
+ * copy over each annotation row returned and also defined on each
+ * sequence, excluding regions not annotated due to gapMap/column
+ * visibility */
+
+ // update calcId if it is not already set on returned annotation
+ for (AlignmentAnnotation annot : returnedAnnot)
+ {
+ if (annot.getCalcId() == null || annot.getCalcId().isEmpty())
+ {
+ annot.setCalcId(action.getFullName());
+ }
+ annot.autoCalculated = action.isAlignmentAnalysis() &&
+ action.getWebService().isInteractive();
+ }
+ job.returnedAnnotations = returnedAnnot;
+ job.featureColours = featureColours;
+ job.featureFilters = featureFilters;
+ var ret = updateResultAnnotation(job, returnedAnnot);
+ var annotations = ret.get0();
+ var transferFeatures = ret.get1();
+ return new AnnotationResult(annotations, transferFeatures, featureColours,
+ featureFilters);
+ }
+
+ private Pair<List<AlignmentAnnotation>, Boolean> updateResultAnnotation(
+ AnnotationJob job, List<AlignmentAnnotation> annotations)
+ {
+ List<AlignmentAnnotation> newAnnots = new ArrayList<>();
+ // update graphGroup for all annotation
+ /* find a graphGroup greater than any existing one, could be moved
+ * to Alignment#getNewGraphGroup() - returns next unused graph group */
+ int graphGroup = 1;
+ if (viewport.getAlignment().getAlignmentAnnotation() != null)
+ {
+ for (var ala : viewport.getAlignment().getAlignmentAnnotation())
+ {
+ graphGroup = Math.max(graphGroup, ala.graphGroup);
+ }
+ }
+ // update graphGroup in the annotation rows returned form service'
+ /* TODO: look at sequence annotation rows and update graph groups in the
+ * case of reference annotation */
+ for (AlignmentAnnotation ala : annotations)
+ {
+ if (ala.graphGroup > 0)
+ ala.graphGroup += graphGroup;
+ SequenceI aseq = null;
+ // transfer sequence refs and adjust gapMap
+ if (ala.sequenceRef != null)
+ {
+ aseq = job.seqNames.get(ala.sequenceRef.getName());
+ }
+ ala.sequenceRef = aseq;
+
+ Annotation[] resAnnot = ala.annotations;
+ boolean[] gapMap = job.gapMap;
+ Annotation[] gappedAnnot = new Annotation[Math.max(
+ viewport.getAlignment().getWidth(), gapMap.length)];
+ for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++)
+ {
+ if (gapMap.length > ap && !gapMap[ap])
+ gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
+ else if (p < resAnnot.length)
+ gappedAnnot[ap] = resAnnot[p++];
+ // is this loop exhaustive of resAnnot?
+ }
+ 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);
+ }
+
+ boolean transferFeatures = false;
+ for (SequenceI sq : job.getInputSequences())
+ {
+ if (!sq.getFeatures().hasFeatures() &&
+ (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
+ continue;
+ transferFeatures = true;
+ SequenceI seq = job.seqNames.get(sq.getName());
+ SequenceI dseq;
+ int start = job.start, end = job.end;
+ boolean[] gapMap = job.gapMap;
+ ContiguousI seqRange = seq.findPositions(start, end);
+ while ((dseq = seq).getDatasetSequence() != null)
+ {
+ seq = seq.getDatasetSequence();
+ }
+ List<ContiguousI> sourceRange = new ArrayList<>();
+ if (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);
+ }
+
+ return new Pair<>(newAnnots, transferFeatures);
+ }
+
+ @Override
+ public AnnotationResult getResult()
+ {
+ return result;
+ }
+
+ @Override
+ public void cancel()
+ {
+ setStatus(JobStatus.CANCELLED);
+ if (worker != null)
+ {
+ worker.stop();
+ }
+ cancelJobs();
+ }
+
+ public void cancelJobs()
+ {
+ for (BaseJob job : jobs)
+ {
+ if (!job.isCompleted())
+ {
+ try
+ {
+ if (job.getServerJob() != null)
+ {
+ client.cancel(job.getServerJob());
+ }
+ job.setStatus(JobStatus.CANCELLED);
+ } catch (IOException e)
+ {
+ Cache.log.error(String.format(
+ "failed to cancel job %s", job.getServerJob()), e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ var status = taskStatus != null ? taskStatus.name() : "UNSET";
+ return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, status);
+ }
+}
--- /dev/null
+package jalview.ws2.actions.api;
+
+import java.util.EnumSet;
+import java.util.List;
+
+import javax.swing.Icon;
+
+import jalview.viewmodel.AlignmentViewport;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.api.CredentialType;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.api.WebService;
+
+/**
+ * {@code Action} object represents an executable action that the web service
+ * can perform. Actions are factories for {@link TaskI} objects which are
+ * created by {@link #perform} method. Actions are instantiated by
+ * {@link WebServiceDiscovererI} from the service definition obtained from the
+ * server and are added to and provided by the {@link WebService}.
+ *
+ * Majority of web services will have a single action only, however multiple
+ * actions providing variation to job execution are possible e.g. align and
+ * realign actions of ClustalO service.
+ *
+ * @author mmwarowny
+ *
+ * @param <R>
+ * task result type
+ */
+public interface ActionI<R>
+{
+ /**
+ * Get the web service containing this action.
+ *
+ * @return containing web service
+ */
+ WebService<? extends ActionI<R>> getWebService();
+
+ /**
+ * Get the name of the action. Typically, it should be the same as the name of
+ * the service.
+ *
+ * @return action name
+ */
+ String getName();
+
+ /**
+ * Get the full name of the action consisting of the service name and the
+ * action name if present.
+ *
+ * @return full name of this action
+ */
+ default String getFullName()
+ {
+ var name = getName();
+ if (name == null || name.isEmpty())
+ return getWebService().getName();
+ else
+ return getWebService().getName() + " " + name;
+ }
+
+ /**
+ * Get the tooltip for the action which contains extra details about the
+ * action.
+ *
+ * @return action tooltip
+ */
+ String getTooltip();
+
+ /**
+ * Get the subcategory this action belongs to. Can be used to group or
+ * separate multiple actions.
+ *
+ * @return action subcategory
+ */
+ String getSubcategory();
+
+ /**
+ * Get the minimum number of sequences this action requires to run or -1 for
+ * no minimum. Actions may still run if the requirement is not met, but may
+ * produce meaningless results.
+ *
+ * @return minimum required number of sequences
+ */
+ int getMinSequences();
+
+ /**
+ * Get the maximum number of sequences this action requires to run or -1 for
+ * no maximum. Actions may still run if the requirement is not met, but may
+ * produce meaningless or incomplete results.
+ *
+ * @return maximum required number of sequences
+ */
+ int getMaxSequences();
+
+ /**
+ * Return if this action allows protein sequences.
+ *
+ * @return {@code true} if protein sequences are allowed
+ */
+ boolean doAllowProtein();
+
+ /**
+ * Return if this action allows nucleotide sequences.
+ *
+ * @return {@code true} if nucleotide sequences are allowed
+ */
+ boolean doAllowNucleotide();
+
+ /**
+ * Get the set of credentials required to run the action.
+ *
+ * @return required credentials
+ */
+ EnumSet<CredentialType> getRequiredCredentials();
+
+ /**
+ * Run the action, create and start a new task with provided viewport,
+ * arguments and credentials and attach the handler to the task. The
+ * implementations of this method are responsible for starting the task using
+ * execution method appropriate for the action class.
+ *
+ * @param viewport
+ * current alignment viewport
+ * @param args
+ * job parameters appropriate for the service
+ * @param credentials
+ * optional user credentials
+ * @param handler
+ * event handler attached to the new task
+ * @return new running task
+ */
+ TaskI<R> perform(AlignmentViewport viewport, List<ArgumentI> args,
+ Credentials credentials, TaskEventListener<R> handler);
+
+ /**
+ * Return if the action is currently active for the given viewport. Active
+ * actions refer to interactive services which are registered to run
+ * repeatedly on viewport changes. This method has no use for one-shot
+ * services and should always return {@code false} in that case.
+ *
+ * @param viewport
+ * viewport being checked for interactive services
+ * @return if there are interactive services registered for viewport
+ */
+ boolean isActive(AlignmentViewport viewport);
+}
--- /dev/null
+package jalview.ws2.actions.api;
+
+import jalview.util.MathUtils;
+import jalview.ws2.api.JobStatus;
+
+public interface JobI
+{
+ /**
+ * Get a unique job id used internally by jalview.
+ *
+ * @return unique job id
+ */
+ long getInternalId();
+
+ /**
+ * Get the status of this job
+ *
+ * @return job status
+ */
+ JobStatus getStatus();
+
+ /**
+ * Get the log for this job
+ *
+ * @return sub job log
+ */
+ String getLog();
+
+ /**
+ * Get the error log for this job.
+ *
+ * @return sub job error log
+ */
+ String getErrorLog();
+}
--- /dev/null
+package jalview.ws2.actions.api;
+
+import java.util.List;
+
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.api.WebServiceJobHandle;
+
+/**
+ * The listener interface for receiving relevant job progress and state change
+ * events on the task. The listener object is registered with the task on its
+ * creation in {@link ActionI#perform} method. An event is generated when the
+ * task is started, restarted, completed, fails with an exception or its global
+ * status changes. Additional, sub-job related, events are emitted when the
+ * sub-job status, log or error log changes.
+ *
+ * @author mmwarowny
+ *
+ * @param <T>
+ */
+public interface TaskEventListener<T>
+{
+ /**
+ * Invoked when the task has been started. The {@code subJobs} parameter
+ * contains a complete list of sub-jobs for that run. Note that restartable
+ * tasks may invoke this method multiple times with different set of sub-jobs.
+ *
+ * @param source
+ * task this event originates from
+ * @param subJobs
+ * list of sub-jobs for this run
+ */
+ void taskStarted(TaskI<T> source, List<? extends JobI> subJobs);
+
+ /**
+ * Invoked when the global task status has changed.
+ *
+ * @param source
+ * task this event originates from
+ * @param status
+ * new task status
+ */
+ void taskStatusChanged(TaskI<T> source, JobStatus status);
+
+ /**
+ * Invoked when the task has completed. If the task completed with a result,
+ * that result is passed in the call argument, otherwise, a {@code null} value
+ * is given.
+ *
+ * @param source
+ * task this event originates from
+ * @param result
+ * computation result or null if result not present
+ */
+ void taskCompleted(TaskI<T> source, T result);
+
+ /**
+ * Invoked when an unhandled exception has occurred during task execution.
+ *
+ * @param source
+ * task this event originates from
+ * @param e
+ * exception
+ */
+ void taskException(TaskI<T> source, Exception e);
+
+ /**
+ * Invoked when the task had been restarted. This event is only applicable to
+ * restartable tasks and will precede each {@link #taskStarted} after the
+ * first one.
+ *
+ * @param source
+ * task this event originates from
+ */
+ void taskRestarted(TaskI<T> source);
+
+ /**
+ * Invoked when the status of a sub-job has changed.
+ *
+ * @param source
+ * task this event originates form
+ * @param job
+ * sub-job that has been updated
+ * @param status
+ * new job status
+ */
+ void subJobStatusChanged(TaskI<T> source, JobI job, JobStatus status);
+
+ /**
+ * Invoked when a log string of the sub-job has changed.
+ *
+ * @param source
+ * task this event originates form
+ * @param job
+ * sub-job that has been updated
+ * @param log
+ * new log string
+ */
+ void subJobLogChanged(TaskI<T> source, JobI job, String log);
+
+ /**
+ * Invoked when an error log string of the sub-job has changed.
+ *
+ * @param source
+ * task this event originates form
+ * @param job
+ * sub-job that has been updated
+ * @param log
+ * new log string
+ */
+ void subJobErrorLogChanged(TaskI<T> source, JobI job, String log);
+}
--- /dev/null
+package jalview.ws2.actions.api;
+
+import java.util.List;
+
+import jalview.ws2.api.JobStatus;
+
+/**
+ * {@code TaskI} objects represent running services. Tasks are created by
+ * concrete implementations of {@link ActionI} and provide a view of the state
+ * of the underlying job(s).
+ *
+ * @author mmwarowny
+ *
+ * @param <T>
+ * task result type
+ */
+public interface TaskI<T>
+{
+ /**
+ * Get the universal identifier of this task.
+ *
+ * @return identifier
+ */
+ long getUid();
+
+ /**
+ * Get the current status of the task. The resultant status should be a
+ * combination of individual sub-job statuses.
+ *
+ * @return global status of
+ */
+ JobStatus getStatus();
+
+ /**
+ * Get the current list of sub-jobs of that task.
+ *
+ * @return sub-jobs
+ */
+ List<? extends JobI> getSubJobs();
+
+ /**
+ * Get the last result of the task or {@code null} if not present. Note that
+ * the result is subject to change for restartable tasks.
+ *
+ * @return last task result
+ */
+ T getResult();
+
+ /**
+ * Cancel the task, stop all sub-jobs running on a server and stop all threads
+ * managing this task.
+ */
+ void cancel();
+}
--- /dev/null
+package jalview.ws2.api;
+
+public enum CredentialType
+{
+ EMAIL, USERNAME, PASSWORD;
+}
--- /dev/null
+package jalview.ws2.api;
+
+import java.util.Objects;
+
+public final class Credentials
+{
+ String username = null;
+ String email = null;
+ String password = null;
+ private static final Credentials EMPTY = new Credentials();
+
+ private Credentials() {
+ }
+
+ public static final Credentials empty()
+ {
+ return EMPTY;
+ }
+
+ public static final Credentials usingEmail(String email) {
+ Objects.requireNonNull(email);
+ if (email.isEmpty())
+ throw new IllegalArgumentException("empty email");
+ Credentials credentials = new Credentials();
+ credentials.email = email;
+ return credentials;
+ }
+
+ public static final Credentials usingEmail(String email, String password) {
+ Objects.requireNonNull(email);
+ Objects.requireNonNull(password);
+ if (email.isEmpty())
+ throw new IllegalArgumentException("empty email");
+ Credentials credentials = new Credentials();
+ credentials.email = email;
+ credentials.password = password;
+ return credentials;
+ }
+
+ public static final Credentials usingUsername(String username, String password) {
+ Objects.requireNonNull(username);
+ Objects.requireNonNull(password);
+ if (username.isEmpty())
+ throw new IllegalArgumentException("empty username");
+ Credentials credentials = new Credentials();
+ credentials.username = username;
+ credentials.password = password;
+ return credentials;
+ }
+}
--- /dev/null
+package jalview.ws2.api;
+
+public enum JobStatus
+{
+ /** Job has invalid inputs and cannot be started. */
+ INVALID,
+ /** Job is created and ready for submission. */
+ READY,
+ /** Job has been submitted and awaits processing. */
+ SUBMITTED,
+ /** Job has been queued for execution */
+ QUEUED,
+ /** Job is running */
+ RUNNING,
+ /** Job has completed successfully. */
+ COMPLETED,
+ /** Job has finished with errors. */
+ FAILED,
+ /** Job has been cancelled by the user. */
+ CANCELLED,
+ /** Job cannot be processed due to server error. */
+ SERVER_ERROR,
+ /** Job status cannot be determined. */
+ UNKNOWN;
+
+ /**
+ * Returns true if the status corresponds to the job completed due to normal
+ * termination, error or cancellation.
+ *
+ * @return {@value true} if status corresponds to a finished job.
+ */
+ public boolean isDone()
+ {
+ switch (this)
+ {
+ case INVALID:
+ case COMPLETED:
+ case FAILED:
+ case CANCELLED:
+ case SERVER_ERROR:
+ return true;
+ case READY:
+ case SUBMITTED:
+ case QUEUED:
+ case RUNNING:
+ case UNKNOWN:
+ return false;
+ default:
+ throw new AssertionError("non-exhaustive switch statement");
+ }
+ }
+
+ /**
+ * A precedence order of job statuses used to compute the overall task status.
+ */
+ public static final JobStatus[] statusPrecedence = {
+ JobStatus.INVALID, // all must be invalid for task to be invalid
+ JobStatus.COMPLETED, // all but invalid must be completed for task to be
+ // completed
+ JobStatus.UNKNOWN, // unknown prevents successful completion but not
+ // running or failure
+ JobStatus.READY,
+ JobStatus.SUBMITTED,
+ JobStatus.QUEUED,
+ JobStatus.RUNNING,
+ JobStatus.CANCELLED, // if any is terminated unsuccessfully, the task is
+ // failed
+ JobStatus.FAILED,
+ JobStatus.SERVER_ERROR
+ };
+}
--- /dev/null
+package jalview.ws2.api;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws2.actions.api.ActionI;
+
+import static java.util.Objects.requireNonNull;
+
+public class WebService<A extends ActionI<?>>
+{
+ public static class Builder<A extends ActionI<?>>
+ {
+ private URL url;
+
+ private String clientName;
+
+ private String category;
+
+ private String name;
+
+ private String description = "";
+
+ private boolean interactive = false;
+
+ private ParamDatastoreI paramDatastore;
+
+ private Class<A> actionClass;
+
+ public Builder<A> url(URL val)
+ {
+ url = val;
+ return this;
+ }
+
+ public Builder<A> clientName(String val)
+ {
+ clientName = val;
+ return this;
+ }
+
+ public Builder<A> category(String val)
+ {
+ category = val;
+ return this;
+ }
+
+ public Builder<A> name(String val)
+ {
+ name = val;
+ return this;
+ }
+
+ public Builder<A> description(String val)
+ {
+ description = val;
+ return this;
+ }
+
+ public Builder<A> interactive(boolean val)
+ {
+ interactive = val;
+ return this;
+ }
+
+ public Builder<A> paramDatastore(ParamDatastoreI val)
+ {
+ paramDatastore = val;
+ return this;
+ }
+
+ public Builder<A> actionClass(Class<A> val)
+ {
+ actionClass = val;
+ return this;
+ }
+
+ public WebService<A> build()
+ {
+ return new WebService<A>(this);
+ }
+ }
+
+ private final URL url;
+
+ private final String clientName;
+
+ private final String category;
+
+ private final String name;
+
+ private final String description;
+
+ private final boolean interactive;
+
+ private final ParamDatastoreI paramDatastore;
+
+ private final List<A> actions;
+
+ private final Class<A> actionClass;
+
+ protected WebService(Builder<A> builder)
+ {
+ requireNonNull(builder.url);
+ requireNonNull(builder.clientName);
+ requireNonNull(builder.category);
+ requireNonNull(builder.name);
+ requireNonNull(builder.paramDatastore);
+ requireNonNull(builder.actionClass);
+ this.url = builder.url;
+ this.clientName = builder.clientName;
+ this.category = builder.category;
+ this.name = builder.name;
+ this.description = builder.description;
+ this.interactive = builder.interactive;
+ this.paramDatastore = builder.paramDatastore;
+ this.actions = new ArrayList<>();
+ this.actionClass = builder.actionClass;
+ }
+
+ public static <A extends ActionI<?>> Builder<A> newBuilder()
+ {
+ return new Builder<A>();
+ }
+
+ public void addAction(A action)
+ {
+ this.actions.add(action);
+ }
+
+ public void addActions(Collection<? extends A> actions)
+ {
+ this.actions.addAll(actions);
+ }
+
+ public URL getUrl()
+ {
+ return url;
+ }
+
+ public String getClientName()
+ {
+ return clientName;
+ }
+
+ public String getCategory()
+ {
+ return category;
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public String getDescription()
+ {
+ return description;
+ }
+
+ public boolean isInteractive()
+ {
+ return interactive;
+ }
+
+ public ParamDatastoreI getParamDatastore()
+ {
+ return paramDatastore;
+ }
+
+ public List<A> getActions()
+ {
+ return actions;
+ }
+
+ public Class<A> getActionClass()
+ {
+ return actionClass;
+ }
+}
--- /dev/null
+package jalview.ws2.api;
+
+import java.util.Date;
+
+/**
+ * {@code WebServiceJob} represents a job running on a remote server. The object
+ * contains all the information needed to associate the job with an originating
+ * client and url, service being run and to poll the job and retrieve the
+ * results from the server. The {@code WebServiceJob} object is provided by the
+ * {@link WebServiceClientI#submit} method when the job is created.
+ *
+ * @see WebServiceClientI
+ *
+ * @author mmwarowny
+ */
+public class WebServiceJobHandle
+{
+ /** Name of the related client */
+ private final String serviceClient;
+
+ /** Name of the related service */
+ private final String serviceName;
+
+ /** URL the job is valid for */
+ private final String url;
+
+ /** External job id as given by the server */
+ private final String jobId;
+
+ private Date creationTime = new Date();
+
+ public WebServiceJobHandle(String serviceClient, String serviceName,
+ String url, String jobId)
+ {
+ this.serviceClient = serviceClient;
+ this.serviceName = serviceName;
+ this.url = url;
+ this.jobId = jobId;
+ }
+
+ /**
+ * Get a URL this job originates from.
+ *
+ * @return job URL
+ */
+ public String getUrl()
+ {
+ return url;
+ }
+
+ /**
+ * Get an id assigned to the job by the server.
+ *
+ * @return job id handle
+ */
+ public String getJobId()
+ {
+ return jobId;
+ }
+
+ /**
+ * @return Job creation time
+ */
+ public Date getCreationTime()
+ {
+ return creationTime;
+ }
+
+ public String toString()
+ {
+ return String.format("%s:%s [%s] Created %s", serviceClient, serviceName,
+ jobId, creationTime);
+ }
+}
--- /dev/null
+package jalview.ws2.client.api;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jalview.bin.Cache;
+import jalview.ws2.actions.api.ActionI;
+import jalview.ws2.api.WebService;
+
+public abstract class AbstractWebServiceDiscoverer implements WebServiceDiscovererI
+{
+ // TODO: we can use linked hash map to group and retrieve services by type.
+ protected List<WebService<?>> services = List.of();
+
+ @Override
+ public List<WebService<?>> getServices()
+ {
+ return services;
+ }
+
+ @Override
+ public <A extends ActionI<?>> List<WebService<A>> getServices(Class<A> type)
+ {
+ List<WebService<A>> list = new ArrayList<>();
+ for (WebService<?> service : services)
+ {
+ if (service.getActionClass().equals(type))
+ {
+ @SuppressWarnings("unchecked")
+ WebService<A> _service = (WebService<A>) service;
+ list.add(_service);
+ }
+ }
+ return list;
+ }
+
+ @Override
+ public List<URL> getUrls()
+ {
+ String key = getUrlsPropertyKey();
+ if (key == null)
+ // unmodifiable urls list, return default
+ return List.of(getDefaultUrl());
+ String surls = Cache.getProperty(key);
+ if (surls == null)
+ return List.of(getDefaultUrl());
+ String[] urls = surls.split(",");
+ ArrayList<URL> valid = new ArrayList<>(urls.length);
+ for (String url : urls)
+ {
+ try
+ {
+ valid.add(new URL(url));
+ } catch (MalformedURLException e)
+ {
+ Cache.log.warn(String.format(
+ "Problem whilst trying to make a URL from '%s'. " +
+ "This was probably due to malformed comma-separated-list " +
+ "in the %s entry of ${HOME}/.jalview-properties",
+ Objects.toString(url, "<null>"), key));
+ Cache.log.debug("Exception occurred while reading url list", e);
+ }
+ }
+ return valid;
+ }
+
+ @Override
+ public void setUrls(List<URL> wsUrls)
+ {
+ String key = getUrlsPropertyKey();
+ if (key == null)
+ throw new UnsupportedOperationException("setting urls not supported");
+ if (wsUrls != null && !wsUrls.isEmpty())
+ {
+ String[] surls = new String[wsUrls.size()];
+ var iter = wsUrls.iterator();
+ for (int i = 0; iter.hasNext(); i++)
+ surls[i] = iter.next().toString();
+ Cache.setProperty(key, String.join(",", surls));
+ }
+ else
+ {
+ Cache.removeProperty(key);
+ }
+ }
+
+ /**
+ * Get the key in jalview property file where the urls for this discoverer are
+ * stored. Return null if modifying urls is not supported.
+ *
+ * @return urls entry key
+ */
+ protected abstract String getUrlsPropertyKey();
+
+ /**
+ * Get the default url for web service discovery for this discoverer.
+ *
+ * @return default discovery url
+ */
+ protected abstract URL getDefaultUrl();
+
+ @Override
+ public boolean hasServices()
+ {
+ return !isRunning() && services.size() > 0;
+ }
+
+ private static final int END = 0x01;
+ private static final int BEGIN = 0x02;
+ private static final int AGAIN = 0x04;
+ private final AtomicInteger state = new AtomicInteger(END);
+ private CompletableFuture<List<WebService<?>>> discoveryTask = new CompletableFuture<>();
+
+ @Override
+ public boolean isRunning()
+ {
+ return (state.get() & (BEGIN | AGAIN)) != 0;
+ }
+
+ @Override
+ public boolean isDone()
+ {
+ return state.get() == END && discoveryTask.isDone();
+ }
+
+ @Override
+ public synchronized final CompletableFuture<List<WebService<?>>> startDiscoverer()
+ {
+ Cache.log.debug("Requesting service discovery");
+ while (true)
+ {
+ if (state.get() == AGAIN)
+ {
+ return discoveryTask;
+ }
+ if (state.compareAndSet(END, BEGIN) || state.compareAndSet(BEGIN, AGAIN))
+ {
+ Cache.log.debug("State changed to " + state.get());
+ final var oldTask = discoveryTask;
+ CompletableFuture<List<WebService<?>>> task = oldTask
+ .handleAsync((_r, _e) -> {
+ Cache.log.info("Reloading services for " + this);
+ fireServicesChanged(services = Collections.emptyList());
+ var allServices = new ArrayList<WebService<?>>();
+ for (var url : getUrls())
+ {
+ Cache.log.info("Fetching list of services from " + url);
+ try
+ {
+ allServices.addAll(fetchServices(url));
+ }
+ catch (IOException e)
+ {
+ Cache.log.error("Failed to get services from " + url, e);
+ }
+ }
+ return services = allServices;
+ });
+ task.<Void>handle((services, exception) -> {
+ while (true)
+ {
+ if (state.get() == END)
+ // should never happen, throw exception to break the loop just in case
+ throw new AssertionError();
+ if (state.compareAndSet(BEGIN, END) || state.compareAndSet(AGAIN, BEGIN))
+ Cache.log.debug("Discovery ended, state is " + state.get());
+ break;
+ }
+ if (services != null)
+ fireServicesChanged(services);
+ return null;
+ });
+ Cache.log.debug("Spawned task " + task);
+ Cache.log.debug("Killing task " + oldTask);
+ oldTask.cancel(false);
+ return discoveryTask = task;
+ }
+ }
+ }
+
+ protected abstract List<WebService<?>> fetchServices(URL url) throws IOException;
+
+ private List<ServicesChangeListener> listeners = new ArrayList<>();
+
+ private void fireServicesChanged(List<WebService<?>> services)
+ {
+ for (var listener : listeners)
+ {
+ try
+ {
+ listener.servicesChanged(this, services);
+ }
+ catch (Exception e)
+ {
+ Cache.log.warn(e);
+ }
+ }
+ }
+
+ @Override
+ public final void addServicesChangeListener(ServicesChangeListener listener)
+ {
+ listeners.add(listener);
+ }
+
+ @Override
+ public final void removeServicesChangeListener(ServicesChangeListener listener)
+ {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getName();
+ }
+}
--- /dev/null
+package jalview.ws2.client.api;
+
+import jalview.ws2.actions.alignment.AlignmentProviderI;
+
+/**
+ * A client interface for alignment services combining {@link WebServiceClientI}
+ * and {@link AlignmentProviderI} functionality into one interface.
+ * Alignment services use this interface to issue queries to the server.
+ *
+ * @author mmwarowny
+ */
+public interface AlignmentWebServiceClientI extends WebServiceClientI, AlignmentProviderI
+{
+
+}
--- /dev/null
+package jalview.ws2.client.api;
+
+import jalview.ws2.actions.annotation.AnnotationProviderI;
+
+/**
+ * A mixin interface used by annotation services combining
+ * {@link WebServiceClientI} and {@link AnnotationProviderI} functionality into
+ * one interface. Annotation services use this interface to issue queries to the
+ * server.
+ *
+ * @author mmwarowny
+ */
+public interface AnnotationWebServiceClientI extends WebServiceClientI, AnnotationProviderI
+{
+
+}
--- /dev/null
+package jalview.ws2.client.api;
+
+import java.io.IOException;
+import java.util.List;
+
+import jalview.datamodel.SequenceI;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.api.WebServiceJobHandle;
+
+/**
+ * A common interface for all web service clients that provide methods to get
+ * the URL of the server the client is talking to, submit new jobs to the server
+ * as well as poll or cancel the running jobs. This interface does not provide
+ * means to retrieve job results as those may differ between web services.
+ * Specialized sub-interfaces define methods to retrieve job results appropriate
+ * for specific service types.
+ *
+ * @author mmwarowny
+ *
+ */
+public interface WebServiceClientI
+{
+ /**
+ * Get the hostname/url of the remote server which is supplying the service.
+ *
+ * @return host name
+ */
+ String getUrl();
+
+ /**
+ * Get the name of the web service client.
+ *
+ * @return client name
+ */
+ String getClientName();
+
+ /**
+ * Submit new job to the service with the supplied input sequences and
+ * arguments. Optionally, some services may require additional credentials to
+ * run. Implementations should perform all data serialization necessary for
+ * the job submission, start a new job on the remote server and return a
+ * handler for that job.
+ *
+ * @param sequences
+ * input sequences
+ * @param args
+ * user provided arguments
+ * @param credentials
+ * optional user credentials needed to run the job
+ * @return job handler
+ * @throws IOException
+ * submission failed due to a connection error
+ */
+ WebServiceJobHandle submit(List<SequenceI> sequences, List<ArgumentI> args,
+ Credentials credentials) throws IOException;
+
+ /**
+ * Poll the server to get the current status of the job.
+ *
+ * @param job
+ * web service job
+ * @return job status
+ * @throws IOException
+ * server communication error
+ */
+ JobStatus getStatus(WebServiceJobHandle job) throws IOException;
+
+ /**
+ * Retrieve log messages from the server for the job.
+ *
+ * @param job
+ * web service job
+ * @return log content
+ * @throws IOException
+ * server communication error
+ */
+ String getLog(WebServiceJobHandle job) throws IOException;
+
+ /**
+ * Retrieve error log messages from the server for the job.
+ *
+ * @param job
+ * web service job
+ * @return error log content
+ * @throws IOException
+ * server communication error
+ */
+ String getErrorLog(WebServiceJobHandle job) throws IOException;
+
+ /**
+ * Send the cancellation request to the server for the specified job.
+ *
+ * @param job
+ * job to cancel
+ * @throws IOException
+ * server error occurred
+ * @throws UnsupportedOperationException
+ * server does not support job cancellation
+ */
+ void cancel(WebServiceJobHandle job) throws IOException, UnsupportedOperationException;
+}
--- /dev/null
+package jalview.ws2.client.api;
+
+import java.net.URL;
+import java.util.EventListener;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import jalview.ws2.api.WebService;
+
+/**
+ * The discoverer and supplier of web services. The discoverer is responsible
+ * for building and storing {@link jalview.ws2.api.WebService} objects
+ * according to the data retrieved from the servers available at specified urls.
+ * @author mmwarowny
+ *
+ */
+public interface WebServiceDiscovererI extends WebServiceProviderI
+{
+ public static final int STATUS_OK = 1;
+
+ public static final int STATUS_NO_SERVICES = 0;
+
+ public static final int STATUS_INVALID = -1;
+
+ public static final int STATUS_UNKNOWN = -2;
+
+ /**
+ * List the urls used by this discoverer.
+ */
+ List<URL> getUrls();
+
+ /**
+ * Set the list of urls where the discoverer will search for services.
+ */
+ void setUrls(List<URL> wsUrls);
+
+ /**
+ * Test if the url is a valid url for that discoverer.
+ */
+ default boolean testUrl(URL url)
+ {
+ return getStatusForUrl(url) == STATUS_OK;
+ }
+
+ /**
+ * Get the availability status of the services at the url. Return one of the
+ * status codes {@code STATUS_OK}, {@code STATUS_NO_SERVICES},
+ * {@code STATUS_INVALID} or {@code STATUS_UNKNOWN}.
+ *
+ * @return services availability status
+ */
+ int getStatusForUrl(URL url);
+
+ /**
+ * @return {@value true} if there are services available
+ */
+ boolean hasServices();
+
+ /**
+ * Check if service discovery is still in progress. List of services may be
+ * incomplete when the discoverer is running.
+ *
+ * @return whether the discoverer is running
+ */
+ boolean isRunning();
+
+ /**
+ * Check if the discoverer is done searching for services. List of services
+ * should be complete if this methods returns true.
+ *
+ * @return whether the discoverer finished
+ */
+ boolean isDone();
+
+ /**
+ * Start the service discovery and return a future which will be set to the
+ * discovery result when the process is completed. This method should be
+ * called once on startup and then every time the urls list is updated.
+ *
+ * @return services list future result
+ */
+ CompletableFuture<List<WebService<?>>> startDiscoverer();
+
+ /**
+ * An interface for the services list observers.
+ *
+ * @author mmwarowny
+ */
+ @FunctionalInterface
+ interface ServicesChangeListener extends EventListener
+ {
+ /**
+ * Called whenever the services list of the observed discoverer changes with
+ * that discoverer as the first argument and current services list as the
+ * second. The list can be empty if there are no services or the list was
+ * cleared at the beginning of the discovery.
+ *
+ * @param discoverer
+ * @param list
+ */
+ public void servicesChanged(WebServiceDiscovererI discoverer,
+ List<WebService<?>> services);
+ }
+
+ /**
+ * Add a services list observer that will be notified of any changes to the
+ * services list.
+ *
+ * @param listener
+ * services list change listener
+ */
+ public void addServicesChangeListener(ServicesChangeListener listener);
+
+ /**
+ * Remove a listener from the listeners list.
+ *
+ * @param listener
+ * listener to be removed
+ */
+ public void removeServicesChangeListener(ServicesChangeListener listener);
+}
--- /dev/null
+package jalview.ws2.client.api;
+
+import java.util.List;
+
+import jalview.ws2.actions.api.ActionI;
+import jalview.ws2.api.WebService;
+
+/*
+ * A view of services that allows to retrieve the services by the type
+ * of action.
+ */
+public interface WebServiceProviderI
+{
+ /**
+ * Retrieve list of all web services.
+ *
+ * @return all web services
+ */
+ public List<WebService<?>> getServices();
+
+ /**
+ * Retrieve services by their action type.
+ *
+ * @param type
+ * action type
+ * @return list of services
+ */
+ public <A extends ActionI<?>> List<WebService<A>> getServices(Class<A> type);
+}
--- /dev/null
+package jalview.ws2.client.slivka;
+
+import static java.util.Objects.requireNonNullElse;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import com.stevesoft.pat.NotImplementedError;
+
+import jalview.bin.Cache;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.ParamManager;
+import jalview.ws.params.WsParamSetI;
+import jalview.ws.params.simple.BooleanOption;
+import jalview.ws.params.simple.DoubleParameter;
+import jalview.ws.params.simple.IntegerParameter;
+import jalview.ws.params.simple.Option;
+import jalview.ws.params.simple.StringParameter;
+import jalview.ws2.params.SimpleParamDatastore;
+import jalview.ws2.params.SimpleParamSet;
+import uk.ac.dundee.compbio.slivkaclient.Parameter;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaService;
+
+class SlivkaParamStoreFactory
+{
+ private final SlivkaService service;
+ private final ParamManager manager;
+
+ SlivkaParamStoreFactory(SlivkaService service, ParamManager manager)
+ {
+ this.service = service;
+ this.manager = manager;
+ }
+
+ ParamDatastoreI createParamDatastore()
+ {
+ URL url = null;
+ try
+ {
+ url = service.getUrl().toURL();
+ } catch (MalformedURLException e)
+ {
+ Cache.log.warn("Invalid service url " + service.getUrl(), e);
+ }
+ List<WsParamSetI> presets = new ArrayList<>(service.getPresets().size());
+ for (var preset : service.getPresets())
+ {
+ presets.add(createPreset(preset));
+ }
+ List<ArgumentI> arguments = createPresetArguments(Collections.emptyMap());
+ return new SimpleParamDatastore(url, arguments, presets, manager);
+ }
+
+ WsParamSetI createPreset(SlivkaService.Preset preset)
+ {
+ var builder = SimpleParamSet.newBuilder();
+ builder.name(preset.name);
+ builder.description(preset.description);
+ builder.url(service.getUrl().toString());
+ builder.modifiable(false);
+ builder.arguments(createPresetArguments(preset.values));
+ return builder.build();
+ }
+
+ List<ArgumentI> createPresetArguments(Map<String, Object> values)
+ {
+ var args = new ArrayList<ArgumentI>();
+ for (Parameter param : service.getParameters())
+ {
+ if (param instanceof Parameter.IntegerParameter)
+ {
+ args.add(createOption((Parameter.IntegerParameter) param,
+ (Integer) values.get(param.getId())));
+ }
+ else if (param instanceof Parameter.DecimalParameter)
+ {
+ args.add(createOption((Parameter.DecimalParameter) param,
+ (Double) values.get(param.getId())));
+ }
+ else if (param instanceof Parameter.TextParameter)
+ {
+ args.add(createOption((Parameter.TextParameter) param,
+ (String) values.get(param.getId())));
+ }
+ else if (param instanceof Parameter.FlagParameter)
+ {
+ args.add(createOption((Parameter.FlagParameter) param,
+ (Boolean) values.get(param.getId())));
+ }
+ else if (param instanceof Parameter.ChoiceParameter)
+ {
+ Object ovalue = values.get(param.getId());
+ List<String> lvalue = null;
+ if (param.isArray())
+ lvalue = (List<String>) ovalue;
+ else if (ovalue != null)
+ lvalue = List.of((String) ovalue);
+ args.addAll(createChoiceOptions((Parameter.ChoiceParameter) param, lvalue));
+ }
+ else if (param instanceof Parameter.FileParameter)
+ {
+ // args.add(createOption((Parameter.FileParameter) param, null));
+ }
+ else
+ {
+ args.add(createOption(param, values.get(param.getId())));
+ }
+ }
+ return args;
+ }
+
+ private Option createOption(Parameter.IntegerParameter param, Integer value)
+ {
+ var builder = IntegerParameter.newBuilder();
+ setCommonProperties(param, builder);
+ builder.setDefaultValue((Integer) param.getDefault());
+ builder.setValue(value);
+ builder.setBounds(param.getMin(), param.getMax());
+ return builder.build();
+ }
+
+ private Option createOption(Parameter.DecimalParameter param, Double value)
+ {
+ var builder = DoubleParameter.newBuilder();
+ setCommonProperties(param, builder);
+ builder.setDefaultValue((Double) param.getDefault());
+ builder.setValue(value);
+ builder.setBounds(param.getMin(), param.getMax());
+ return builder.build();
+ }
+
+ private Option createOption(Parameter.TextParameter param, String value)
+ {
+ var builder = StringParameter.newBuilder();
+ setCommonProperties(param, builder);
+ builder.setDefaultValue((String) param.getDefault());
+ builder.setValue(value);
+ return builder.build();
+ }
+
+ private Option createOption(Parameter.FlagParameter param, Boolean value)
+ {
+ var builder = BooleanOption.newBuilder();
+ setCommonProperties(param, builder);
+ builder.setDefaultValue((Boolean) param.getDefault());
+ builder.setValue(value);
+ return builder.build();
+ }
+
+ private List<Option> createChoiceOptions(Parameter.ChoiceParameter param, List<String> value)
+ {
+ value = requireNonNullElse(value, Collections.emptyList());
+ if (param.isArray())
+ {
+ /*
+ * Array parameter means that multiple values can be provided.
+ * Use multiple boolean checkboxes to represent the value.
+ */
+ List<Option> options = new ArrayList<>();
+ List<?> selected = requireNonNullElse(
+ (List<?>) param.getDefault(), Collections.emptyList());
+ int i = 0;
+ var builder = BooleanOption.newBuilder();
+ setCommonProperties(param, builder);
+ for (String choice : param.getChoices())
+ {
+ builder.setName(String.format("%s$%d", param.getId(), i++));
+ builder.setLabel(choice);
+ builder.setDefaultValue(selected.contains(choice));
+ builder.setValue(value.contains(choice));
+ builder.setReprValue(choice);
+ options.add(builder.build());
+ }
+ return options;
+ }
+ else
+ {
+ /*
+ * Single value parameter means a single string with limited possible
+ * values can be used.
+ */
+ var builder = StringParameter.newBuilder();
+ setCommonProperties(param, builder);
+ builder.setDefaultValue((String) param.getDefault());
+ if (value.size() > 0)
+ builder.setValue(value.get(0));
+ builder.setPossibleValues(param.getChoices());
+ return List.of(builder.build());
+ }
+ }
+
+ private Option createOption(Parameter.FileParameter param, File value)
+ {
+ throw new NotImplementedError("file paramters are not implemented for slivka");
+ }
+
+ private Option createOption(Parameter param, Object value)
+ {
+ var builder = StringParameter.newBuilder();
+ setCommonProperties(param, builder);
+ if (param.getDefault() != null)
+ builder.setDefaultValue(param.getDefault().toString());
+ if (value != null)
+ builder.setValue(value.toString());
+ return builder.build();
+ }
+
+ private void setCommonProperties(Parameter param, Option.Builder builder)
+ {
+ builder.setName(param.getId());
+ builder.setLabel(param.getName());
+ builder.setDescription(param.getDescription());
+ builder.setRequired(param.isRequired());
+ try
+ {
+ builder.setDetailsUrl(service.getUrl().toURL());
+ } catch (MalformedURLException e)
+ {
+ Cache.log.warn("invalid service url " + service.getUrl(), e);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+package jalview.ws2.client.slivka;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import jalview.api.FeatureColourI;
+import jalview.bin.Cache;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.io.AnnotationFile;
+import jalview.io.DataSourceType;
+import jalview.io.FeaturesFile;
+import jalview.io.FileFormat;
+import jalview.io.FormatAdapter;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.api.WebServiceJobHandle;
+import jalview.ws2.client.api.AlignmentWebServiceClientI;
+import jalview.ws2.client.api.AnnotationWebServiceClientI;
+import jalview.ws2.client.api.WebServiceClientI;
+import uk.ac.dundee.compbio.slivkaclient.Job;
+import uk.ac.dundee.compbio.slivkaclient.Parameter;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaClient;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaService;
+
+import static java.lang.String.format;
+
+public class SlivkaWSClient implements WebServiceClientI
+{
+ final SlivkaService service;
+
+ final SlivkaClient client;
+
+ SlivkaWSClient(SlivkaService service)
+ {
+ this.service = service;
+ this.client = service.getClient();
+ }
+
+ @Override
+ public String getUrl()
+ {
+ return client.getUrl().toString();
+ }
+
+ @Override
+ public String getClientName()
+ {
+ return "slivka";
+ }
+
+ // pattern for matching media types
+ static final Pattern mediaTypePattern = Pattern.compile(
+ "(?:text|application)\\/(?:x-)?([\\w-]+)");
+
+ @Override
+ public WebServiceJobHandle submit(List<SequenceI> sequences,
+ List<ArgumentI> args, Credentials credentials) throws IOException
+ {
+ var request = new uk.ac.dundee.compbio.slivkaclient.JobRequest();
+ for (Parameter param : service.getParameters())
+ {
+ // TODO: restrict input sequences parameter name to "sequences"
+ if (param instanceof Parameter.FileParameter)
+ {
+ Parameter.FileParameter fileParam = (Parameter.FileParameter) param;
+ FileFormat format = null;
+ var match = mediaTypePattern.matcher(fileParam.getMediaType());
+ if (match.find())
+ {
+ String fmt = match.group(1);
+ if (fmt.equalsIgnoreCase("pfam"))
+ format = FileFormat.Pfam;
+ else if (fmt.equalsIgnoreCase("stockholm"))
+ format = FileFormat.Stockholm;
+ else if (fmt.equalsIgnoreCase("clustal"))
+ format = FileFormat.Clustal;
+ else if (fmt.equalsIgnoreCase("fasta"))
+ format = FileFormat.Fasta;
+ }
+ if (format == null)
+ {
+ Cache.log.warn(String.format(
+ "Unknown input format %s, assuming fasta.",
+ fileParam.getMediaType()));
+ format = FileFormat.Fasta;
+ }
+ 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 if (param instanceof Parameter.FileParameter)
+ {
+ request.addFile(paramId, new File(arg.getValue()));
+ }
+ else
+ {
+ request.addData(paramId, arg.getValue());
+ }
+ }
+ }
+ var job = service.submitJob(request);
+ return createJobHandle(job.getId());
+ }
+
+ protected WebServiceJobHandle createJobHandle(String jobId)
+ {
+ return new WebServiceJobHandle(
+ getClientName(), service.getName(), client.getUrl().toString(),
+ jobId);
+ }
+
+ @Override
+ public JobStatus getStatus(WebServiceJobHandle job) throws IOException
+ {
+ var slivkaJob = client.getJob(job.getJobId());
+ return statusMap.getOrDefault(slivkaJob.getStatus(), JobStatus.UNKNOWN);
+ }
+
+ protected static final EnumMap<Job.Status, JobStatus> statusMap = new EnumMap<>(Job.Status.class);
+ static
+ {
+ statusMap.put(Job.Status.PENDING, JobStatus.SUBMITTED);
+ statusMap.put(Job.Status.REJECTED, JobStatus.INVALID);
+ statusMap.put(Job.Status.ACCEPTED, JobStatus.SUBMITTED);
+ statusMap.put(Job.Status.QUEUED, JobStatus.QUEUED);
+ statusMap.put(Job.Status.RUNNING, JobStatus.RUNNING);
+ statusMap.put(Job.Status.COMPLETED, JobStatus.COMPLETED);
+ statusMap.put(Job.Status.INTERRUPTED, JobStatus.CANCELLED);
+ statusMap.put(Job.Status.DELETED, JobStatus.CANCELLED);
+ statusMap.put(Job.Status.FAILED, JobStatus.FAILED);
+ statusMap.put(Job.Status.ERROR, JobStatus.SERVER_ERROR);
+ statusMap.put(Job.Status.UNKNOWN, JobStatus.UNKNOWN);
+ }
+
+ @Override
+ public String getLog(WebServiceJobHandle job) throws IOException
+ {
+ var slivkaJob = client.getJob(job.getJobId());
+ for (var f : slivkaJob.getResults())
+ {
+ if (f.getLabel().equals("log"))
+ {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ f.writeTo(stream);
+ return stream.toString(StandardCharsets.UTF_8);
+ }
+ }
+ return "";
+ }
+
+ @Override
+ public String getErrorLog(WebServiceJobHandle job) throws IOException
+ {
+ var slivkaJob = client.getJob(job.getJobId());
+ for (var f : slivkaJob.getResults())
+ {
+ if (f.getLabel().equals("error-log"))
+ {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ f.writeTo(stream);
+ return stream.toString(StandardCharsets.UTF_8);
+ }
+ }
+ return "";
+ }
+
+ @Override
+ public void cancel(WebServiceJobHandle job)
+ throws IOException, UnsupportedOperationException
+ {
+ Cache.log.warn(
+ "slivka client does not support job cancellation");
+ }
+}
+
+class SlivkaAlignmentWSClient extends SlivkaWSClient
+ implements AlignmentWebServiceClientI
+{
+
+ SlivkaAlignmentWSClient(SlivkaService service)
+ {
+ super(service);
+ }
+
+ @Override
+ public AlignmentI getAlignment(WebServiceJobHandle job) throws IOException
+ {
+ var slivkaJob = client.getJob(job.getJobId());
+ for (var f : slivkaJob.getResults())
+ {
+ // TODO: restrict result file label to "alignment"
+ FileFormat format;
+ var match = mediaTypePattern.matcher(f.getMediaType());
+ if (!match.find())
+ continue;
+ String fmt = match.group(1);
+ if (fmt.equalsIgnoreCase("clustal"))
+ format = FileFormat.Clustal;
+ else if (fmt.equalsIgnoreCase("fasta"))
+ format = FileFormat.Fasta;
+ else
+ continue;
+ return new FormatAdapter().readFile(f.getContentUrl().toString(),
+ DataSourceType.URL, format);
+ }
+ Cache.log.warn("No alignment found on the server");
+ throw new IOException("no alignment found");
+ }
+
+}
+
+class SlivkaAnnotationWSClient extends SlivkaWSClient
+ implements AnnotationWebServiceClientI
+{
+ SlivkaAnnotationWSClient(SlivkaService service)
+ {
+ super(service);
+ }
+
+ @Override
+ public List<AlignmentAnnotation> attachAnnotations(WebServiceJobHandle job,
+ List<SequenceI> sequences, Map<String, FeatureColourI> colours,
+ Map<String, FeatureMatcherSetI> filters) throws IOException
+ {
+ var slivkaJob = client.getJob(job.getJobId());
+ var aln = new Alignment(sequences.toArray(new SequenceI[sequences.size()]));
+ boolean featPresent = false, annotPresent = false;
+ for (var f : slivkaJob.getResults())
+ {
+ // TODO: restrict file label to "annotations" or "features"
+ var match = mediaTypePattern.matcher(f.getMediaType());
+ if (!match.find())
+ continue;
+ String fmt = match.group(1);
+ if (fmt.equalsIgnoreCase("jalview-annotations"))
+ {
+ annotPresent = new AnnotationFile().readAnnotationFileWithCalcId(
+ aln, service.getId(), f.getContentUrl().toString(),
+ DataSourceType.URL);
+ if (annotPresent)
+ Cache.log.debug(format("loaded annotations for %s", service.getId()));
+ }
+ else if (fmt.equalsIgnoreCase("jalview-features"))
+ {
+ FeaturesFile ff = new FeaturesFile(f.getContentUrl().toString(),
+ DataSourceType.URL);
+ featPresent = ff.parse(aln, colours, true);
+ if (featPresent)
+ Cache.log.debug(format("loaded features for %s", service.getId()));
+ }
+ }
+ if (!annotPresent)
+ Cache.log.debug(format("no annotations found for %s", service.getId()));
+ if (!featPresent)
+ Cache.log.debug(format("no features found for %s", service.getId()));
+ return aln.getAlignmentAnnotation() != null ? Arrays.asList(aln.getAlignmentAnnotation())
+ : Collections.emptyList();
+ }
+}
--- /dev/null
+package jalview.ws2.client.slivka;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import jalview.bin.Cache;
+import jalview.ws.params.ParamManager;
+import jalview.ws2.actions.alignment.AlignmentAction;
+import jalview.ws2.actions.annotation.AnnotationAction;
+import jalview.ws2.api.WebService;
+import jalview.ws2.client.api.AbstractWebServiceDiscoverer;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaClient;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaService;
+
+public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer
+{
+ private static final String SLIVKA_HOST_URLS = "SLIVKAHOSTURLS";
+
+ private static final URL DEFAULT_URL;
+ static
+ {
+ try
+ {
+ DEFAULT_URL = new URL("https://www.compbio.dundee.ac.uk/slivka/");
+ } catch (MalformedURLException e)
+ {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static SlivkaWSDiscoverer instance = null;
+
+ private static ParamManager paramManager = null;
+
+ private SlivkaWSDiscoverer()
+ {
+ }
+
+ public static SlivkaWSDiscoverer getInstance()
+ {
+ if (instance == null)
+ instance = new SlivkaWSDiscoverer();
+ return instance;
+ }
+
+ public static void setParamManager(ParamManager manager)
+ {
+ paramManager = manager;
+ }
+
+ @Override
+ public int getStatusForUrl(URL url)
+ {
+ try
+ {
+ List<?> services = new SlivkaClient(url.toString()).getServices();
+ return services.isEmpty() ? STATUS_NO_SERVICES : STATUS_OK;
+ } catch (IOException e)
+ {
+ Cache.log.error("slivka could not retrieve services from " + url, e);
+ return STATUS_INVALID;
+ }
+ }
+
+ @Override
+ protected String getUrlsPropertyKey()
+ {
+ return SLIVKA_HOST_URLS;
+ }
+
+ @Override
+ protected URL getDefaultUrl()
+ {
+ return DEFAULT_URL;
+ }
+
+ @Override
+ protected List<WebService<?>> fetchServices(URL url) throws IOException
+ {
+ ArrayList<WebService<?>> allServices = new ArrayList<>();
+ SlivkaClient slivkaClient;
+ try
+ {
+ slivkaClient = new SlivkaClient(url.toURI());
+ } catch (URISyntaxException e)
+ {
+ throw new MalformedURLException(e.getMessage());
+ }
+ for (var slivkaService : slivkaClient.getServices())
+ {
+ int serviceClass = getServiceClass(slivkaService);
+ if (serviceClass == SERVICE_CLASS_MSA)
+ {
+ var wsb = WebService.<AlignmentAction> newBuilder();
+ initServiceBuilder(slivkaService, wsb);
+ wsb.category("Alignment");
+ wsb.interactive(false);
+ wsb.actionClass(AlignmentAction.class);
+ var msaService = wsb.build();
+
+ boolean canRealign = msaService.getName().contains("lustal");
+ var client = new SlivkaAlignmentWSClient(slivkaService);
+ var actionBuilder = AlignmentAction.newBuilder(client);
+ actionBuilder.name("Alignment");
+ actionBuilder.webService(msaService);
+ if (canRealign)
+ actionBuilder.subcategory("Align");
+ actionBuilder.minSequences(2);
+ msaService.addAction(actionBuilder.build());
+ if (canRealign)
+ {
+ actionBuilder.name("Re-alignment");
+ actionBuilder.subcategory("Realign");
+ actionBuilder.submitGaps(true);
+ msaService.addAction(actionBuilder.build());
+ }
+ allServices.add(msaService);
+ }
+ else if (serviceClass == SERVICE_CLASS_PROT_SEQ_ANALYSIS)
+ {
+ var wsb = WebService.<AnnotationAction> newBuilder();
+ initServiceBuilder(slivkaService, wsb);
+ wsb.category("Protein Disorder");
+ wsb.interactive(false);
+ wsb.actionClass(AnnotationAction.class);
+ var psaService = wsb.build();
+ var client = new SlivkaAnnotationWSClient(slivkaService);
+ var actionBuilder = AnnotationAction.newBuilder(client);
+ actionBuilder.webService(psaService);
+ actionBuilder.name("Analysis");
+ psaService.addAction(actionBuilder.build());
+ allServices.add(psaService);
+ }
+ else if (serviceClass == SERVICE_CLASS_CONSERVATION)
+ {
+ var wsb = WebService.<AnnotationAction> newBuilder();
+ initServiceBuilder(slivkaService, wsb);
+ wsb.category("Conservation");
+ wsb.interactive(true);
+ wsb.actionClass(AnnotationAction.class);
+ var conService = wsb.build();
+ var client = new SlivkaAnnotationWSClient(slivkaService);
+ var actionBuilder = AnnotationAction.newBuilder(client);
+ actionBuilder.webService(conService);
+ actionBuilder.name("");
+ actionBuilder.alignmentAnalysis(true);
+ actionBuilder.requireAlignedSequences(true);
+ actionBuilder.filterSymbols(true);
+ conService.addAction(actionBuilder.build());
+ allServices.add(conService);
+ }
+ else if (serviceClass == SERVICE_CLASS_RNA_SEC_STR_PRED)
+ {
+ var wsb = WebService.<AnnotationAction> newBuilder();
+ initServiceBuilder(slivkaService, wsb);
+ wsb.category("Secondary Structure Prediction");
+ wsb.interactive(true);
+ wsb.actionClass(AnnotationAction.class);
+ var predService = wsb.build();
+ var client = new SlivkaAnnotationWSClient(slivkaService);
+ var actionBuilder = AnnotationAction.newBuilder(client);
+ actionBuilder.webService(predService);
+ actionBuilder.name("Prediction");
+ actionBuilder.minSequences(2);
+ actionBuilder.allowNucleotide(true);
+ actionBuilder.allowProtein(false);
+ actionBuilder.alignmentAnalysis(true);
+ actionBuilder.requireAlignedSequences(true);
+ actionBuilder.filterSymbols(false);
+ predService.addAction(actionBuilder.build());
+ allServices.add(predService);
+ }
+ else
+ {
+ continue;
+ }
+ }
+ return allServices;
+ }
+
+ private void initServiceBuilder(SlivkaService service, WebService.Builder<?> wsBuilder)
+ {
+ try
+ {
+ wsBuilder.url(service.getClient().getUrl().toURL());
+ } catch (MalformedURLException e)
+ {
+ e.printStackTrace();
+ }
+ wsBuilder.clientName("slivka");
+ wsBuilder.name(service.getName());
+ wsBuilder.description(service.getDescription());
+ var storeBuilder = new SlivkaParamStoreFactory(service, paramManager);
+ wsBuilder.paramDatastore(storeBuilder.createParamDatastore());
+ }
+
+ static final int SERVICE_CLASS_UNSUPPORTED = -1;
+
+ static final int SERVICE_CLASS_MSA = 1;
+
+ static final int SERVICE_CLASS_RNA_SEC_STR_PRED = 2;
+
+ static final int SERVICE_CLASS_CONSERVATION = 3;
+
+ static final int SERVICE_CLASS_PROT_SEQ_ANALYSIS = 4;
+
+ static final int SERVICE_CLASS_PROT_SEC_STR_PRED = 5;
+
+ /**
+ * Scan service classifiers starting with operation :: analysis to decide the
+ * operation class.
+ *
+ * @return service class flag
+ */
+ private static int getServiceClass(SlivkaService service)
+ {
+ for (String classifier : service.getClassifiers())
+ {
+ String[] path = classifier.split("\\s*::\\s*");
+ if (path.length < 3 || !path[0].equalsIgnoreCase("operation") ||
+ !path[1].equalsIgnoreCase("analysis"))
+ continue;
+ // classifier is operation :: analysis :: *
+ var tail = path[path.length - 1].toLowerCase();
+ switch (tail)
+ {
+ case "multiple sequence alignment":
+ return SERVICE_CLASS_MSA;
+ case "rna secondary structure prediction":
+ return SERVICE_CLASS_RNA_SEC_STR_PRED;
+ case "sequence alignment analysis (conservation)":
+ return SERVICE_CLASS_CONSERVATION;
+ case "protein sequence analysis":
+ return SERVICE_CLASS_PROT_SEQ_ANALYSIS;
+ case "protein secondary structure prediction":
+ return SERVICE_CLASS_PROT_SEC_STR_PRED;
+ }
+ }
+ return SERVICE_CLASS_UNSUPPORTED;
+ }
+}
--- /dev/null
+package jalview.ws2.gui;
+
+import static java.util.Objects.requireNonNullElse;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JInternalFrame;
+import javax.swing.SwingUtilities;
+
+import jalview.bin.Cache;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AlignmentOrder;
+import jalview.datamodel.HiddenColumns;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+import jalview.gui.JvOptionPane;
+import jalview.gui.SplitFrame;
+import jalview.gui.WebserviceInfo;
+import jalview.util.ArrayUtils;
+import jalview.util.MessageManager;
+import jalview.util.Pair;
+import jalview.ws2.actions.alignment.AlignmentAction;
+import jalview.ws2.actions.alignment.AlignmentResult;
+import jalview.ws2.actions.api.JobI;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.api.WebService;
+import jalview.ws2.helpers.WSClientTaskWrapper;
+
+class AlignmentServiceGuiHandler
+ implements TaskEventListener<AlignmentResult>
+{
+ private final WebService<?> service;
+
+ private final AlignFrame frame;
+
+ private WebserviceInfo infoPanel;
+
+ private String alnTitle; // title of the alignment used in new window
+
+ private JobI[] jobs = new JobI[0];
+
+ private int[] tabs = new int[0];
+
+ private int[] logOffset = new int[0];
+
+ private int[] errLogOffset = new int[0];
+
+ public AlignmentServiceGuiHandler(AlignmentAction action, AlignFrame frame)
+ {
+ this.service = action.getWebService();
+ this.frame = frame;
+ String panelInfo = String.format("%s using service hosted at %s%n%s",
+ service.getName(), service.getUrl(), service.getDescription());
+ infoPanel = new WebserviceInfo(service.getName(), panelInfo, false);
+ String actionName = requireNonNullElse(action.getName(), "Alignment");
+ alnTitle = String.format("%s %s of %s", service.getName(), actionName,
+ frame.getTitle());
+ }
+
+ @Override
+ public void taskStatusChanged(TaskI<AlignmentResult> source, JobStatus status)
+ {
+ switch (status)
+ {
+ case INVALID:
+ infoPanel.setVisible(false);
+ JvOptionPane.showMessageDialog(frame,
+ MessageManager.getString("info.invalid_msa_input_mininfo"),
+ MessageManager.getString("info.invalid_msa_notenough"),
+ JvOptionPane.INFORMATION_MESSAGE);
+ break;
+ case READY:
+ infoPanel.setthisService(new WSClientTaskWrapper(source));
+ infoPanel.setVisible(true);
+ // intentional no break
+ case SUBMITTED:
+ case QUEUED:
+ infoPanel.setStatus(WebserviceInfo.STATE_QUEUING);
+ break;
+ case RUNNING:
+ case UNKNOWN: // unsure what to do with unknown
+ infoPanel.setStatus(WebserviceInfo.STATE_RUNNING);
+ break;
+ case COMPLETED:
+ infoPanel.setProgressBar(
+ MessageManager.getString("status.collecting_job_results"),
+ jobs[0].getInternalId());
+ infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_OK);
+ break;
+ case FAILED:
+ infoPanel.removeProgressBar(jobs[0].getInternalId());
+ infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
+ break;
+ case CANCELLED:
+ infoPanel.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
+ break;
+ case SERVER_ERROR:
+ infoPanel.removeProgressBar(jobs[0].getInternalId());
+ infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
+ break;
+ }
+ }
+
+ @Override
+ public void taskStarted(TaskI<AlignmentResult> source, List<? extends JobI> subJobs)
+ {
+ jobs = subJobs.toArray(new JobI[0]);
+ tabs = new int[subJobs.size()];
+ logOffset = new int[subJobs.size()];
+ errLogOffset = new int[subJobs.size()];
+ for (int i = 0; i < subJobs.size(); i++)
+ {
+ JobI job = jobs[i];
+ int tabIndex = infoPanel.addJobPane();
+ tabs[i] = tabIndex;
+ infoPanel.setProgressName(String.format("region %d", i), tabIndex);
+ infoPanel.setProgressText(tabIndex, alnTitle + "\nJob details\n");
+ // jobs should not have states other than invalid or ready at this point
+ if (job.getStatus() == JobStatus.INVALID)
+ infoPanel.setStatus(tabIndex, WebserviceInfo.STATE_STOPPED_OK);
+ else if (job.getStatus() == JobStatus.READY)
+ infoPanel.setStatus(tabIndex, WebserviceInfo.STATE_QUEUING);
+ }
+ }
+
+ @Override
+ public void taskCompleted(TaskI<AlignmentResult> source, AlignmentResult result)
+ {
+ SwingUtilities.invokeLater(() -> infoPanel.removeProgressBar(jobs[0].getInternalId()));
+ if (result == null)
+ {
+ SwingUtilities.invokeLater(infoPanel::setFinishedNoResults);
+ return;
+ }
+ infoPanel.showResultsNewFrame.addActionListener(evt -> {
+ var aln = result.getAlignment();
+ // copy alignment for each frame to have its own isntance
+ var alnCpy = new Alignment(aln);
+ alnCpy.setGapCharacter(aln.getGapCharacter());
+ alnCpy.setDataset(aln.getDataset());
+ displayResultsNewFrame(alnCpy, result.getAlignmentOrders(),
+ result.getHiddenColumns());
+ });
+ SwingUtilities.invokeLater(infoPanel::setResultsReady);
+ }
+
+ private void displayResultsNewFrame(Alignment aln,
+ List<AlignmentOrder> alorders, HiddenColumns hidden)
+ {
+ AlignFrame newFrame = new AlignFrame(aln, hidden, AlignFrame.DEFAULT_WIDTH,
+ AlignFrame.DEFAULT_HEIGHT);
+ newFrame.getFeatureRenderer().transferSettings(
+ frame.getFeatureRenderer().getSettings());
+ if (alorders.size() > 0)
+ {
+ addSortByMenuItems(newFrame, alorders);
+ }
+
+ var requestingFrame = frame;
+ var splitContainer = requestingFrame.getSplitViewContainer();
+ if (splitContainer != null && splitContainer.getComplement(requestingFrame) != null)
+ {
+ AlignmentI complement = splitContainer.getComplement(requestingFrame);
+ String complementTitle = splitContainer.getComplementTitle(requestingFrame);
+ Alignment copyComplement = new Alignment(complement);
+ copyComplement.setGapCharacter(complement.getGapCharacter());
+ copyComplement.setDataset(complement.getDataset());
+ copyComplement.alignAs(aln);
+ if (copyComplement.getHeight() > 0)
+ {
+ newFrame.setTitle(alnTitle);
+ AlignFrame newFrame2 = new AlignFrame(copyComplement,
+ AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
+ newFrame2.setTitle(complementTitle);
+ String linkedTitle = MessageManager.getString("label.linked_view_title");
+ JInternalFrame splitFrame = new SplitFrame(
+ aln.isNucleotide() ? newFrame : newFrame2,
+ aln.isNucleotide() ? newFrame2 : newFrame);
+ Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1);
+ return;
+ }
+ }
+ // no split frame or failed to create complementary alignment
+ Desktop.addInternalFrame(newFrame, alnTitle, AlignFrame.DEFAULT_WIDTH,
+ AlignFrame.DEFAULT_HEIGHT);
+ }
+
+ private void addSortByMenuItems(AlignFrame frame, List<AlignmentOrder> alorders)
+ {
+ if (alorders.size() == 1)
+ {
+ frame.addSortByOrderMenuItem(service.getName() + " Ordering",
+ alorders.get(0));
+ return;
+ }
+ BitSet collected = new BitSet(alorders.size());
+ for (int i = 0, N = alorders.size(); i < N; i++)
+ {
+ if (collected.get(i))
+ continue;
+ var regions = new ArrayList<String>();
+ var order = alorders.get(i);
+ for (int j = i; j < N; j++)
+ {
+ if (!collected.get(j) && alorders.get(j).equals(order))
+ {
+ regions.add(Integer.toString(j + 1));
+ collected.set(j);
+ }
+ }
+ var orderName = String.format("%s Region %s Ordering",
+ service.getName(), String.join(",", regions));
+ frame.addSortByOrderMenuItem(orderName, order);
+ }
+ }
+
+ @Override
+ public void taskException(TaskI<AlignmentResult> source, Exception e)
+ {
+ Cache.log.error(String.format("Service %s raised an exception.", service.getName()), e);
+ infoPanel.appendProgressText(e.getMessage());
+ }
+
+ @Override
+ public void taskRestarted(TaskI<AlignmentResult> source)
+ {
+ // alignment services are not restartable
+ }
+
+ @Override
+ public void subJobStatusChanged(TaskI<AlignmentResult> source, JobI job, JobStatus status)
+ {
+ int i = ArrayUtils.indexOf(jobs, job);
+ assert i >= 0 : "job does not exist";
+ if (i < 0)
+ // safeguard that should not happen irl
+ return;
+ int wsStatus;
+ switch (status)
+ {
+ case INVALID:
+ case COMPLETED:
+ wsStatus = WebserviceInfo.STATE_STOPPED_OK;
+ break;
+ case READY:
+ case SUBMITTED:
+ case QUEUED:
+ wsStatus = WebserviceInfo.STATE_QUEUING;
+ break;
+ case RUNNING:
+ case UNKNOWN:
+ wsStatus = WebserviceInfo.STATE_RUNNING;
+ break;
+ case FAILED:
+ wsStatus = WebserviceInfo.STATE_STOPPED_ERROR;
+ break;
+ case CANCELLED:
+ wsStatus = WebserviceInfo.STATE_CANCELLED_OK;
+ break;
+ case SERVER_ERROR:
+ wsStatus = WebserviceInfo.STATE_STOPPED_SERVERERROR;
+ break;
+ default:
+ throw new AssertionError("Non-exhaustive switch statement");
+ }
+ infoPanel.setStatus(tabs[i], wsStatus);
+ }
+
+ @Override
+ public void subJobLogChanged(TaskI<AlignmentResult> source, JobI job, String log)
+ {
+ int i = ArrayUtils.indexOf(jobs, job);
+ assert i >= 0 : "job does not exist";
+ if (i < 0)
+ // safeguard that should never happen
+ return;
+ infoPanel.appendProgressText(tabs[i], log.substring(logOffset[i]));
+ }
+
+ @Override
+ public void subJobErrorLogChanged(TaskI<AlignmentResult> source, JobI job, String log)
+ {
+ int i = ArrayUtils.indexOf(jobs, job);
+ assert i >= 0 : "job does not exist";
+ if (i < 0)
+ // safeguard that should never happen
+ return;
+ infoPanel.appendProgressText(tabs[i], log.substring(errLogOffset[i]));
+ }
+
+}
--- /dev/null
+package jalview.ws2.gui;
+
+import java.util.List;
+
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignmentPanel;
+import jalview.gui.IProgressIndicator;
+import jalview.gui.IProgressIndicatorHandler;
+import jalview.ws2.actions.annotation.AnnotationAction;
+import jalview.ws2.actions.annotation.AnnotationResult;
+import jalview.ws2.actions.api.JobI;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.JobStatus;
+
+public class AnnotationServiceGuiHandler
+ implements TaskEventListener<AnnotationResult>
+{
+ private final AlignFrame alignFrame;
+
+ private final AlignmentPanel alignPanel;
+
+ private final IProgressIndicator progressIndicator;
+
+ private final AnnotationAction action;
+
+ public AnnotationServiceGuiHandler(AnnotationAction action, AlignFrame frame)
+ {
+ this.alignFrame = frame;
+ this.alignPanel = frame.alignPanel;
+ this.progressIndicator = frame;
+ this.action = action;
+ }
+
+ @Override
+ public void taskStarted(TaskI<AnnotationResult> source, List<? extends JobI> subJobs)
+ {
+ progressIndicator.registerHandler(source.getUid(),
+ new IProgressIndicatorHandler()
+ {
+ @Override
+ public boolean cancelActivity(long id)
+ {
+ source.cancel();
+ return true;
+ }
+
+ @Override
+ public boolean canCancel()
+ {
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public void taskStatusChanged(TaskI<AnnotationResult> source, JobStatus status)
+ {
+ switch (status)
+ {
+ case INVALID:
+ case COMPLETED:
+ case CANCELLED:
+ case FAILED:
+ case SERVER_ERROR:
+ progressIndicator.removeProgressBar(source.getUid());
+ break;
+ case READY:
+ case SUBMITTED:
+ case QUEUED:
+ case RUNNING:
+ case UNKNOWN:
+ progressIndicator.addProgressBar(source.getUid(), action.getFullName());
+ break;
+ }
+ }
+
+ @Override
+ public void taskCompleted(TaskI<AnnotationResult> source, AnnotationResult result)
+ {
+ if (result == null)
+ return;
+ if (result.getTransferFeatures() && alignFrame.alignPanel == alignPanel)
+ {
+ alignFrame.getViewport().setShowSequenceFeatures(true);
+ alignFrame.setMenusForViewport();
+ }
+ alignPanel.adjustAnnotationHeight();
+ }
+
+ @Override
+ public void taskException(TaskI<AnnotationResult> source, Exception e)
+ {
+
+ }
+
+ @Override
+ public void taskRestarted(TaskI<AnnotationResult> source)
+ {
+
+ }
+
+ @Override
+ public void subJobStatusChanged(TaskI<AnnotationResult> source, JobI job, JobStatus status)
+ {
+
+ }
+
+ @Override
+ public void subJobLogChanged(TaskI<AnnotationResult> source, JobI job, String log)
+ {
+
+ }
+
+ @Override
+ public void subJobErrorLogChanged(TaskI<AnnotationResult> source, JobI job, String log)
+ {
+
+ }
+}
--- /dev/null
+package jalview.ws2.gui;
+
+import java.awt.Color;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.concurrent.CompletionStage;
+
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JLabel;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.ToolTipManager;
+import javax.swing.border.EmptyBorder;
+
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+import jalview.gui.JvSwingUtils;
+import jalview.gui.WsJobParameters;
+import jalview.util.MessageManager;
+import jalview.viewmodel.AlignmentViewport;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.WsParamSetI;
+import jalview.ws2.actions.alignment.AlignmentAction;
+import jalview.ws2.actions.annotation.AnnotationAction;
+import jalview.ws2.actions.api.ActionI;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.api.WebService;
+import jalview.ws2.client.api.WebServiceProviderI;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNullElse;
+
+public class WebServicesMenuManager
+{
+ private final JMenu menu;
+
+ private final AlignFrame frame;
+
+ private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
+
+ private JMenuItem noServicesItem = new JMenuItem("No services available");
+ {
+ inProgressItem.setEnabled(false);
+ inProgressItem.setVisible(false);
+ noServicesItem.setEnabled(false);
+ }
+
+ private Map<String, WeakReference<TaskI<?>>> interactiveTasks = new HashMap<>();
+
+ public WebServicesMenuManager(String name, AlignFrame frame)
+ {
+ this.frame = frame;
+ menu = new JMenu(name);
+ menu.add(inProgressItem);
+ menu.add(noServicesItem);
+ }
+
+ public JMenu getMenu()
+ {
+ return menu;
+ }
+
+ public void setNoServices(boolean noServices)
+ {
+ noServicesItem.setVisible(noServices);
+ }
+
+ public void setInProgress(boolean inProgress)
+ {
+ inProgressItem.setVisible(inProgress);
+ }
+
+ public void setServices(WebServiceProviderI services)
+ {
+ menu.removeAll();
+ // services grouped by their category
+ Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
+ Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
+ for (WebService<?> service : services.getServices())
+ {
+ var map = service.isInteractive() ? interactiveServices : oneshotServices;
+ map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
+ .add(service);
+ }
+ var allKeysSet = new HashSet<>(oneshotServices.keySet());
+ allKeysSet.addAll(interactiveServices.keySet());
+ var allKeys = new ArrayList<>(allKeysSet);
+ allKeys.sort(Comparator.naturalOrder());
+ for (String category : allKeys)
+ {
+ var categoryMenu = new JMenu(category);
+ var oneshot = oneshotServices.get(category);
+ if (oneshot != null)
+ addOneshotEntries(oneshot, categoryMenu);
+ var interactive = interactiveServices.get(category);
+ if (interactive != null)
+ {
+ if (oneshot != null)
+ categoryMenu.addSeparator();
+ addInteractiveEntries(interactive, categoryMenu);
+ }
+ menu.add(categoryMenu);
+ }
+ menu.add(inProgressItem);
+ menu.add(noServicesItem);
+ }
+
+ private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
+ {
+ services.sort(Comparator
+ .<WebService<?>, String> comparing(s -> s.getUrl().toString())
+ .thenComparing(WebService::getName));
+ URL lastHost = null;
+ for (WebService<?> service : services)
+ {
+ // if new host differs from the last one, add entry separating them
+ URL host = service.getUrl();
+ if (!host.equals(lastHost))
+ {
+ if (lastHost != null)
+ menu.addSeparator();
+ var item = new JMenuItem(host.toString());
+ item.setForeground(Color.BLUE);
+ item.addActionListener(e -> Desktop.showUrl(host.toString()));
+ menu.add(item);
+ lastHost = host;
+ }
+ menu.addSeparator();
+ // group actions by their subcategory, sorted
+ var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
+ for (ActionI<?> action : service.getActions())
+ {
+ actionsByCategory
+ .computeIfAbsent(
+ Objects.requireNonNullElse(action.getSubcategory(), ""),
+ k -> new ArrayList<>())
+ .add(action);
+ }
+ actionsByCategory.forEach((k, v) -> {
+ // create submenu named {subcategory} with {service} or use root menu
+ var atMenu = k.isEmpty() ? menu : new JMenu(String.format("%s with %s", k, service.getName()));
+ if (atMenu != menu)
+ menu.add(atMenu); // add only if submenu
+ // sort actions by name pulling nulls to the front
+ v.sort(Comparator.comparing(
+ ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
+ for (ActionI<?> action : v)
+ {
+ addEntriesForAction(action, atMenu, atMenu == menu);
+ }
+ });
+ }
+ }
+
+ private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
+ {
+ var service = action.getWebService();
+ String itemName;
+ if (isTopLevel)
+ {
+ itemName = service.getName();
+ if (action.getName() != null && !action.getName().isEmpty())
+ itemName += " " + action.getName();
+ }
+ else
+ {
+ if (action.getName() == null || action.getName().isEmpty())
+ itemName = "Run";
+ else
+ itemName = action.getName();
+ }
+ var datastore = service.getParamDatastore();
+ {
+ String text = itemName;
+ if (datastore.hasParameters() || datastore.hasPresets())
+ text += " with defaults";
+ JMenuItem item = new JMenuItem(text);
+ item.addActionListener(e -> {
+ runAction(action, frame.getCurrentView(), Collections.emptyList(),
+ Credentials.empty());
+ });
+ menu.add(item);
+ }
+ if (datastore.hasParameters())
+ {
+ JMenuItem item = new JMenuItem("Edit settings and run...");
+ item.addActionListener(e -> {
+ openEditParamsDialog(datastore, null, null).thenAccept(args -> {
+ if (args != null)
+ runAction(action, frame.getCurrentView(), args, Credentials.empty());
+ });
+ });
+ menu.add(item);
+ }
+ var presets = datastore.getPresets();
+ if (presets != null && presets.size() > 0)
+ {
+ final var presetsMenu = new JMenu(MessageManager.formatMessage(
+ "label.run_with_preset_params", service.getName()));
+ final int dismissDelay = ToolTipManager.sharedInstance()
+ .getDismissDelay();
+ final int QUICK_TOOLTIP = 1500;
+ for (var preset : presets)
+ {
+ var item = new JMenuItem(preset.getName());
+ item.addMouseListener(new MouseAdapter()
+ {
+ @Override
+ public void mouseEntered(MouseEvent evt)
+ {
+ ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
+ }
+
+ @Override
+ public void mouseExited(MouseEvent evt)
+ {
+ ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
+ }
+ });
+ String tooltipTitle = MessageManager.getString(
+ preset.isModifiable() ? "label.user_preset" : "label.service_preset");
+ String tooltip = String.format("<strong>%s</strong><br/>%s",
+ tooltipTitle, preset.getDescription());
+ tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
+ item.setToolTipText(tooltip);
+ item.addActionListener(event -> {
+ runAction(action, frame.getCurrentView(), preset.getArguments(),
+ Credentials.empty());
+ });
+ presetsMenu.add(item);
+ }
+ menu.add(presetsMenu);
+ }
+ }
+
+ private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
+ {
+ Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (var service : services)
+ {
+ byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
+ .add(service);
+ }
+ for (var entry : byServiceName.entrySet())
+ {
+ var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
+ group.appendTo(menu);
+ }
+ }
+
+ private class InteractiveServiceEntryGroup
+ {
+ JLabel serviceLabel;
+
+ JMenuItem urlItem = new JMenuItem();
+
+ JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
+
+ JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
+
+ JMenu presetsMenu = new JMenu("Change preset");
+
+ JMenu alternativesMenu = new JMenu("Choose action");
+ {
+ urlItem.setForeground(Color.BLUE);
+ urlItem.setVisible(false);
+ serviceItem.setVisible(false);
+ editParamsItem.setVisible(false);
+ presetsMenu.setVisible(false);
+ }
+
+ InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
+ {
+ serviceLabel = new JLabel(name);
+ serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
+ buildAlternativesMenu(services);
+ }
+
+ private void buildAlternativesMenu(List<WebService<?>> services)
+ {
+ var menu = alternativesMenu;
+ services.sort(Comparator
+ .<WebService<?>, String> comparing(s -> s.getUrl().toString())
+ .thenComparing(s -> s.getName()));
+ URL lastHost = null;
+ for (var service : services)
+ {
+ // Adding url "separator" before each group
+ URL host = service.getUrl();
+ if (!host.equals(lastHost))
+ {
+ if (lastHost != null)
+ menu.addSeparator();
+ var item = new JMenuItem(host.toString());
+ item.setForeground(Color.BLUE);
+ item.addActionListener(e -> Desktop.showUrl(host.toString()));
+ menu.add(item);
+ lastHost = host;
+ }
+ menu.addSeparator();
+ var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
+ for (ActionI<?> action : service.getActions())
+ {
+ actionsByCategory
+ .computeIfAbsent(
+ requireNonNullElse(action.getSubcategory(), ""),
+ k -> new ArrayList<>())
+ .add(action);
+ }
+ actionsByCategory.forEach((key, actions) -> {
+ var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
+ boolean topLevel = atMenu == menu;
+ if (!topLevel)
+ menu.add(atMenu);
+ actions.sort(Comparator.comparing(
+ a -> a.getName(),
+ Comparator.nullsFirst(Comparator.naturalOrder())));
+ for (ActionI<?> action : actions)
+ {
+ var item = new JMenuItem(action.getFullName());
+ item.addActionListener(e -> setAlternative(action));
+ atMenu.add(item);
+ }
+ });
+ }
+ }
+
+ private void setAlternative(ActionI<?> action)
+ {
+ final var arguments = new ArrayList<ArgumentI>();
+ final WsParamSetI[] lastPreset = { null };
+
+ // update selected url menu item
+ String url = action.getWebService().getUrl().toString();
+ urlItem.setText(url);
+ urlItem.setVisible(true);
+ for (var l : urlItem.getActionListeners())
+ urlItem.removeActionListener(l);
+ urlItem.addActionListener(e -> Desktop.showUrl(url));
+
+ // update selected service menu item
+ serviceItem.setText(action.getFullName());
+ serviceItem.setVisible(true);
+ for (var l : serviceItem.getActionListeners())
+ serviceItem.removeActionListener(l);
+ WebService<?> service = action.getWebService();
+ serviceItem.addActionListener(e -> {
+ if (serviceItem.getState())
+ {
+ cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
+ Credentials.empty());
+ }
+ else
+ {
+ cancelInteractive(service.getName());
+ }
+ });
+ serviceItem.setSelected(true);
+
+ // update edit parameters menu item
+ var datastore = service.getParamDatastore();
+ editParamsItem.setVisible(datastore.hasParameters());
+ for (var l : editParamsItem.getActionListeners())
+ editParamsItem.removeActionListener(l);
+ if (datastore.hasParameters())
+ {
+ editParamsItem.addActionListener(e -> {
+ openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments)
+ .thenAccept(args -> {
+ if (args != null)
+ {
+ lastPreset[0] = null;
+ arguments.clear();
+ arguments.addAll(args);
+ cancelAndRunInteractive(action, frame.getCurrentView(),
+ arguments, Credentials.empty());
+ }
+ });
+ });
+ }
+
+ // update presets menu
+ presetsMenu.removeAll();
+ presetsMenu.setEnabled(datastore.hasPresets());
+ if (datastore.hasPresets())
+ {
+ for (WsParamSetI preset : datastore.getPresets())
+ {
+ var item = new JMenuItem(preset.getName());
+ item.addActionListener(e -> {
+ lastPreset[0] = preset;
+ cancelAndRunInteractive(action, frame.getCurrentView(),
+ preset.getArguments(), Credentials.empty());
+ });
+ presetsMenu.add(item);
+ }
+ }
+
+ cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
+ Credentials.empty());
+ }
+
+ void appendTo(JMenu menu)
+ {
+ menu.add(serviceLabel);
+ menu.add(urlItem);
+ menu.add(serviceItem);
+ menu.add(editParamsItem);
+ menu.add(presetsMenu);
+ menu.add(alternativesMenu);
+ }
+ }
+
+ private void cancelInteractive(String wsName)
+ {
+ var taskRef = interactiveTasks.get(wsName);
+ if (taskRef != null && taskRef.get() != null)
+ taskRef.get().cancel();
+ interactiveTasks.put(wsName, null);
+ }
+
+ private void cancelAndRunInteractive(ActionI<?> action,
+ AlignmentViewport viewport, List<ArgumentI> args, Credentials credentials)
+ {
+ var wsName = action.getWebService().getName();
+ cancelInteractive(wsName);
+ var task = runAction(action, viewport, args, credentials);
+ interactiveTasks.put(wsName, new WeakReference<>(task));
+ }
+
+ private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
+ List<ArgumentI> args, Credentials credentials)
+ {
+ // casting and instance checks can be avoided with some effort,
+ // let them be for now.
+ if (action instanceof AlignmentAction)
+ {
+ // TODO: test if selection contains enough sequences
+ var _action = (AlignmentAction) action;
+ var handler = new AlignmentServiceGuiHandler(_action, frame);
+ return _action.perform(viewport, args, credentials, handler);
+ }
+ if (action instanceof AnnotationAction)
+ {
+ var _action = (AnnotationAction) action;
+ var handler = new AnnotationServiceGuiHandler(_action, frame);
+ return _action.perform(viewport, args, credentials, handler);
+ }
+ throw new IllegalArgumentException(
+ String.format("Illegal action type %s", action.getClass().getName()));
+ }
+
+ private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
+ ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
+ {
+ final WsJobParameters jobParams;
+ if (preset == null && arguments != null && arguments.size() > 0)
+ jobParams = new WsJobParameters(paramStore, null, arguments);
+ else
+ jobParams = new WsJobParameters(paramStore, preset, null);
+ if (preset != null)
+ jobParams.setName(MessageManager.getString(
+ "label.adjusting_parameters_for_calculation"));
+ var stage = jobParams.showRunDialog();
+ return stage.thenApply(startJob -> {
+ if (!startJob)
+ return null; // null if cancelled
+ if (jobParams.getPreset() != null)
+ return jobParams.getPreset().getArguments();
+ if (jobParams.isServiceDefaults())
+ return Collections.emptyList();
+ else
+ return jobParams.getJobParams();
+ });
+ }
+}
--- /dev/null
+package jalview.ws2.helpers;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import jalview.ws2.actions.BaseJob;
+import jalview.ws2.actions.api.JobI;
+import jalview.ws2.api.JobStatus;
+
+/**
+ * A property change listener to be used by web service tasks that delegates all
+ * sub-job related events from {@link BaseJob} subclasses to
+ * {@link TaskEventSupport}. Tasks can create one instance of this class with
+ * their event handler as a delegate and add it as a property change listener to
+ * each sub-job supporting property change listeners. It ensures that an
+ * appropriate {@code fireSubJob*Changed} method of the delegate object will be
+ * called whenever a {@link PropertyChagneEvent} is emitted by the sub-job.
+ *
+ * @author mmwarowny
+ *
+ * @param <T>
+ * result type of the task
+ */
+public class DelegateJobEventListener<T> implements PropertyChangeListener
+{
+ private final TaskEventSupport<T> delegate;
+
+ public DelegateJobEventListener(TaskEventSupport<T> delegate)
+ {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt)
+ {
+ switch (evt.getPropertyName())
+ {
+ case "status":
+ statusChanged(evt);
+ break;
+ case "log":
+ logChanged(evt);
+ break;
+ case "errorLog":
+ errorLogChanged(evt);
+ break;
+ default:
+ throw new AssertionError(String.format(
+ "illegal property name \"%s\"", evt.getPropertyName()));
+ }
+ }
+
+ private void statusChanged(PropertyChangeEvent evt)
+ {
+ JobI job = (JobI) evt.getSource();
+ JobStatus status = (JobStatus) evt.getNewValue();
+ delegate.fireSubJobStatusChanged(job, status);
+ }
+
+ private void logChanged(PropertyChangeEvent evt)
+ {
+ JobI job = (JobI) evt.getSource();
+ String log = (String) evt.getNewValue();
+ delegate.fireSubJobLogChanged(job, log);
+ }
+
+ private void errorLogChanged(PropertyChangeEvent evt)
+ {
+ JobI job = (JobI) evt.getSource();
+ String errorLog = (String) evt.getNewValue();
+ delegate.fireSubJobErrorLogChanged(job, errorLog);
+ }
+}
--- /dev/null
+package jalview.ws2.helpers;
+
+import java.util.List;
+
+import jalview.ws2.actions.api.JobI;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.JobStatus;
+
+public class TaskEventSupport<T>
+{
+ TaskI<T> source;
+ TaskEventListener<T> handler;
+
+ public TaskEventSupport(TaskI<T> source, TaskEventListener<T> handler)
+ {
+ this.source = source;
+ this.handler = handler;
+ }
+
+ public void fireTaskStarted(List<? extends JobI> subJobs)
+ {
+ handler.taskStarted(source, subJobs);
+ }
+
+ public void fireTaskStatusChanged(JobStatus status)
+ {
+ handler.taskStatusChanged(source, status);
+ }
+
+ public void fireTaskCompleted(T result)
+ {
+ handler.taskCompleted(source, result);
+ }
+
+ public void fireTaskException(Exception e)
+ {
+ handler.taskException(source, e);
+ }
+
+ public void fireTaskRestarted()
+ {
+ handler.taskRestarted(source);
+ }
+
+ public void fireSubJobStatusChanged(JobI job, JobStatus status)
+ {
+ handler.subJobStatusChanged(source, job, status);
+ }
+
+ public void fireSubJobLogChanged(JobI job, String log)
+ {
+ handler.subJobLogChanged(source, job, log);
+ }
+
+ public void fireSubJobErrorLogChanged(JobI job, String log)
+ {
+ handler.subJobErrorLogChanged(source, job, log);
+ }
+}
--- /dev/null
+package jalview.ws2.helpers;
+
+import jalview.gui.WebserviceInfo;
+import jalview.ws.WSClientI;
+import jalview.ws2.actions.api.TaskI;
+
+/**
+ * A simple wrapper around the {@link TaskI} implementing {@link WSClientI}. Its
+ * main purpose is to delegate the call to {@link #cancelJob} to the underlying
+ * task.
+ *
+ * @author mmwarowny
+ */
+public class WSClientTaskWrapper implements WSClientI
+{
+ private TaskI<?> delegate;
+
+ private boolean cancellable;
+
+ private boolean canMerge;
+
+ public WSClientTaskWrapper(TaskI<?> task, boolean cancellable, boolean canMerge)
+ {
+ this.delegate = task;
+ this.cancellable = cancellable;
+ this.canMerge = canMerge;
+ }
+
+ public WSClientTaskWrapper(TaskI<?> task)
+ {
+ this(task, true, false);
+ }
+
+ @Override
+ public boolean isCancellable()
+ {
+ return cancellable;
+ }
+
+ @Override
+ public boolean canMergeResults()
+ {
+ return canMerge;
+ }
+
+ @Override
+ public void cancelJob()
+ {
+ delegate.cancel();
+ }
+
+}
--- /dev/null
+package jalview.ws2.params;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import jalview.ws.params.ArgumentI;
+
+/**
+ * A minimal bean implementing {@link ArgumentI} which stores argument
+ * name, label and value. It's mainly used to marshal and unmarshal
+ * parameter values of a preset.
+ *
+ * @author mmwarowny
+ *
+ */
+@XmlRootElement(name = "parameter")
+class ArgumentBean implements ArgumentI
+{
+ String name;
+
+ String label;
+
+ String value;
+
+ ArgumentBean()
+ {
+ this.name = null;
+ this.label = null;
+ this.value = null;
+ }
+
+ ArgumentBean(ArgumentI copyof)
+ {
+ this.name = copyof.getName();
+ this.label = copyof.getLabel();
+ this.value = copyof.getValue();
+ }
+
+ @XmlAttribute
+ @Override
+ public String getName()
+ {
+ return name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ @XmlElement
+ @Override
+ public String getLabel()
+ {
+ return label;
+ }
+
+ public void setLabel(String label)
+ {
+ this.label = label;
+ }
+
+ @XmlElement
+ @Override
+ public String getValue()
+ {
+ return value;
+ }
+
+ @Override
+ public void setValue(String selectedItem)
+ {
+ this.value = selectedItem;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("Parameter(name=%s, value=%s)", name, value);
+ }
+}
\ No newline at end of file
--- /dev/null
+package jalview.ws2.params;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import jalview.ws.params.ArgumentI;
+
+/**
+ * A wrapper of {@link ArgumentBean} list that can be marshaled or unmarshalled.
+ * Used by {@link SimpleParamDatastore} to read and store parameters in a file.
+ *
+ * @see ArgumentBean
+ * @see SimpleParamDatastore
+ * @author mmwarowny
+ */
+@XmlRootElement(name = "arguments")
+class ArgumentBeanList
+{
+ @XmlElement(name = "argument")
+ public List<ArgumentBean> arguments = Collections.emptyList();
+
+ ArgumentBeanList()
+ {
+ }
+
+ ArgumentBeanList(List<ArgumentBean> arguments)
+ {
+ this.arguments = arguments;
+ }
+
+ static ArgumentBeanList fromList(List<? extends ArgumentI> list)
+ {
+ var args = new ArrayList<ArgumentBean>();
+ for (var item : list)
+ args.add(item instanceof ArgumentBean ? (ArgumentBean) item : new ArgumentBean(item));
+ return new ArgumentBeanList(args);
+ }
+
+ @Override
+ public String toString()
+ {
+ var elements = new String[arguments.size()];
+ for (int i = 0; i < arguments.size(); i++)
+ elements[i] = arguments.toString();
+ return "[" + String.join(", ", elements) + "]";
+ }
+}
\ No newline at end of file
--- /dev/null
+package jalview.ws2.params;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Unmarshaller;
+
+import jalview.bin.Cache;
+import jalview.util.MessageManager;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.ParamManager;
+import jalview.ws.params.WsParamSetI;
+
+/**
+ * A web client agnostic parameters datastore that provides view of the
+ * parameters and delegates parameters storage to {@link ParamManager}
+ * if given. Parameter datastore maintains the applicable service url
+ * the list of service parameters and both presets and user defined
+ * parameter sets
+ */
+public class SimpleParamDatastore implements ParamDatastoreI
+{
+ protected URL serviceUrl;
+ protected List<ArgumentI> parameters;
+ protected List<SimpleParamSet> servicePresets;
+ protected List<SimpleParamSet> userPresets = new ArrayList<>();
+ protected ParamManager manager;
+
+ /**
+ * Create new parameter datastore bound to the specified url with
+ * given service parameters and presets. Additionally, a parameters
+ * manager may be provided that will be used to load and store
+ * user parameter sets.
+ *
+ * @param serviceUrl applicable url
+ * @param parameters service parameters
+ * @param presets unmodifiable service presets
+ * @param manager parameter manager used to load and store user presets
+ */
+ public SimpleParamDatastore(URL serviceUrl, List<ArgumentI> parameters,
+ List<? extends WsParamSetI> presets, ParamManager manager)
+ {
+ this.serviceUrl = serviceUrl;
+ this.parameters = Collections.unmodifiableList(new ArrayList<>(parameters));
+ this.servicePresets = new ArrayList<>(presets.size());
+ for (var preset : presets)
+ {
+ if (preset instanceof SimpleParamSet)
+ servicePresets.add((SimpleParamSet) preset);
+ else
+ servicePresets.add(new SimpleParamSet(preset));
+ }
+ this.servicePresets = Collections.unmodifiableList(this.servicePresets);
+ this.manager = manager;
+ if (manager != null)
+ _initManager(manager);
+ }
+
+ private void _initManager(ParamManager manager)
+ {
+ manager.registerParser(serviceUrl.toString(), this);
+ WsParamSetI[] paramSets = manager.getParameterSet(null, serviceUrl.toString(),
+ true, false);
+ if (paramSets != null)
+ {
+ for (WsParamSetI paramSet : paramSets)
+ {
+ // TODO: handle mismatch between preset and current parameters
+ if (paramSet instanceof SimpleParamSet)
+ userPresets.add((SimpleParamSet) paramSet);
+ else
+ {
+ userPresets.add(new SimpleParamSet(paramSet));
+ Cache.log.warn(String.format(
+ "Parameter set instance type %s is not applicable to service"
+ + "at %s.", paramSet.getClass(), serviceUrl));
+ }
+ }
+ }
+ }
+
+ @Override
+ public List<WsParamSetI> getPresets()
+ {
+ List<WsParamSetI> presets = new ArrayList<>();
+ presets.addAll(servicePresets);
+ presets.addAll(userPresets);
+ return presets;
+ }
+
+ @Override
+ public SimpleParamSet getPreset(String name)
+ {
+ SimpleParamSet preset = null;
+ preset = getUserPreset(name);
+ if (preset != null)
+ return preset;
+ preset = getServicePreset(name);
+ if (preset != null)
+ return preset;
+ return null;
+ }
+
+ public SimpleParamSet getUserPreset(String name)
+ {
+ for (SimpleParamSet preset : userPresets)
+ {
+ if (name.equals(preset.getName()))
+ return preset;
+ }
+ return null;
+ }
+
+ public SimpleParamSet getServicePreset(String name)
+ {
+ for (SimpleParamSet preset : servicePresets)
+ {
+ if (name.equals(preset.getName()))
+ return preset;
+ }
+ return null;
+ }
+
+ @Override
+ public List<ArgumentI> getServiceParameters()
+ {
+ return parameters;
+ }
+
+ @Override
+ public boolean presetExists(String name)
+ {
+ return getPreset(name) != null;
+ }
+
+ @Override
+ public void deletePreset(String name)
+ {
+ var userPreset = getUserPreset(name);
+ if (userPreset != null)
+ {
+ userPresets.remove(userPreset);
+ if (manager != null)
+ {
+ manager.deleteParameterSet(userPreset);
+ }
+ }
+ else if (getServicePreset(name) != null)
+ {
+ throw new RuntimeException(MessageManager.getString(
+ "error.implementation_error_attempt_to_delete_service_preset"));
+ }
+ else
+ {
+ Cache.log.warn("Implementation error: no preset to delete");
+ }
+ }
+
+ @Override
+ public void storePreset(String presetName, String text, List<ArgumentI> jobParams)
+ {
+ var builder = SimpleParamSet.newBuilder();
+ builder.name(presetName);
+ builder.description(text);
+ builder.arguments(jobParams);
+ builder.url(serviceUrl.toString());
+ builder.modifiable(true);
+ var preset = builder.build();
+ userPresets.add(preset);
+ if (manager != null)
+ manager.storeParameterSet(preset);
+ }
+
+ @Override
+ public void updatePreset(String oldName, String newName, String text, List<ArgumentI> jobParams)
+ {
+ var preset = getPreset(oldName != null ? oldName : newName);
+ if (preset == null)
+ throw new RuntimeException(MessageManager.formatMessage(
+ "error.implementation_error_cannot_locate_oldname_presetname",
+ oldName, newName));
+ preset.setName(newName);
+ preset.setDescription(text);
+ preset.setArguments(jobParams);
+ preset.setApplicableUrls(new String[] { serviceUrl.toString() });
+ if (manager != null)
+ manager.storeParameterSet(preset);
+ }
+
+ @Override
+ public WsParamSetI parseServiceParameterFile(String name, String description,
+ String[] serviceURL, String parameters)
+ throws IOException
+ {
+ var builder = SimpleParamSet.newBuilder();
+ builder.name(name);
+ builder.description(description);
+ builder.urls(serviceURL);
+ builder.modifiable(true);
+ Unmarshaller unmarshaller;
+ try
+ {
+ var ctx = JAXBContext.newInstance(ArgumentBeanList.class);
+ unmarshaller = ctx.createUnmarshaller();
+ } catch (JAXBException e)
+ {
+ throw new RuntimeException(e);
+ }
+ ArgumentBeanList argList;
+ try
+ {
+ argList = (ArgumentBeanList) unmarshaller.unmarshal(new StringReader(parameters));
+ } catch (JAXBException | ClassCastException e)
+ {
+ throw new IOException("Unable to load parameters from file", e);
+ }
+ builder.arguments(argList.arguments);
+ return builder.build();
+ }
+
+ @Override
+ public String generateServiceParameterFile(WsParamSetI pset) throws IOException
+ {
+ Marshaller marshaller;
+ try
+ {
+ var ctx = JAXBContext.newInstance(ArgumentBeanList.class);
+ marshaller = ctx.createMarshaller();
+ marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+ marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
+ } catch (JAXBException e)
+ {
+ throw new RuntimeException(e);
+ }
+ ArgumentBeanList argList = ArgumentBeanList.fromList(pset.getArguments());
+ var out = new ByteArrayOutputStream();
+ try
+ {
+ marshaller.marshal(argList, out);
+ } catch (JAXBException e)
+ {
+ throw new IOException("Unable to generate parameters file", e);
+ }
+ return out.toString();
+ }
+
+}
--- /dev/null
+package jalview.ws2.params;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.WsParamSetI;
+
+/**
+ * A simple, web service client agnostic, representation of parameter sets.
+ * Instances are created from the service data fetched from the server or from
+ * the user preset files. This implementation of {@link WsParamSetI} is meant to
+ * decouple parameter set representation form specific clients.
+ *
+ * @author mmwarowny
+ *
+ */
+public class SimpleParamSet implements WsParamSetI
+{
+ /**
+ * A convenience builder of {@link SimpleParamSet} objects.
+ *
+ * @author mmwarowny
+ */
+ public static class Builder
+ {
+ private String name = "default";
+
+ private String description = "";
+
+ private List<String> applicableUrls = new ArrayList<>();
+
+ private boolean modifiable = false;
+
+ private List<ArgumentI> arguments = new ArrayList<>();
+
+ public Builder()
+ {
+ }
+
+ /**
+ * Set a name of parameter set.
+ *
+ * @param val
+ * name
+ */
+ public void name(String val)
+ {
+ name = val;
+ }
+
+ /**
+ * Set a description of parameter set.
+ *
+ * @param val
+ * description
+ */
+ public void description(String val)
+ {
+ description = val;
+ }
+
+ /**
+ * Add a url to applicable urls for parameter set.
+ *
+ * @param val
+ * applicable url
+ */
+ public void url(String val)
+ {
+ applicableUrls.add(val);
+ }
+
+ /**
+ * Set all applicable urls for parameter set. Current url list will be
+ * replaced by provided urls.
+ *
+ * @param val
+ * applicable urls
+ */
+ public void urls(String[] val)
+ {
+ applicableUrls.clear();
+ for (String url : val)
+ applicableUrls.add(url);
+ }
+
+ /**
+ * Set modifiable flag for parameter set.
+ *
+ * @param val
+ * modifiable
+ */
+ public void modifiable(boolean val)
+ {
+ modifiable = val;
+ }
+
+ /**
+ * Add an argument to the preset arguments.
+ *
+ * @param val
+ * argument to be added
+ */
+ public void argument(ArgumentI val)
+ {
+ arguments.add(val);
+ }
+
+ /**
+ * Set arguments for parameter set. Current parameters list will be
+ * replaced by provided arguments.
+ *
+ * @param val
+ * arguments to be added
+ */
+ public void arguments(List<? extends ArgumentI> val)
+ {
+ arguments.clear();
+ arguments.addAll(val);
+ }
+
+ /**
+ * Build a new {@link SimpleParamSet} object from the current state of this
+ * builder.
+ *
+ * @return new paramset instance
+ */
+ public SimpleParamSet build()
+ {
+ return new SimpleParamSet(this);
+ }
+ }
+
+ protected String name;
+
+ protected String description;
+
+ protected String[] applicableUrls;
+
+ protected String sourceFile;
+
+ protected boolean modifiable;
+
+ protected List<ArgumentI> arguments;
+
+ protected SimpleParamSet(Builder builder)
+ {
+ this.name = builder.name;
+ this.description = builder.description;
+ this.applicableUrls = builder.applicableUrls.toArray(new String[0]);
+ this.sourceFile = null;
+ this.modifiable = builder.modifiable;
+ setArguments(builder.arguments);
+ }
+
+ /**
+ * Create a copy of the provided paramset. The new instance has the same
+ * properties as the original paramset. The arguments list is a shallow copy
+ * of the original arguments.
+ *
+ * @param copy
+ */
+ public SimpleParamSet(WsParamSetI copy)
+ {
+ this.name = copy.getName();
+ this.description = copy.getDescription();
+ var urls = copy.getApplicableUrls();
+ this.applicableUrls = Arrays.copyOf(urls, urls.length);
+ this.sourceFile = copy.getSourceFile();
+ this.modifiable = copy.isModifiable();
+ setArguments(copy.getArguments());
+ }
+
+ /**
+ * Create a new instance of the parameter set builder.
+ *
+ * @return new parameter set builder
+ */
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
+ @Override
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Set a human readable name for this parameter set.
+ *
+ * @param name
+ * new name
+ */
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ @Override
+ public String getDescription()
+ {
+ return description;
+ }
+
+ /**
+ * Set additional notes for this parameter set.
+ *
+ * @param description
+ * additional notes
+ */
+ public void setDescription(String description)
+ {
+ this.description = description;
+ }
+
+ @Override
+ public String[] getApplicableUrls()
+ {
+ return applicableUrls;
+ }
+
+ /**
+ * Set the list of service endpoints which this parameter set is valid for.
+ *
+ * @param urls
+ * new service endpoints
+ */
+ public void setApplicableUrls(String[] urls)
+ {
+ this.applicableUrls = urls;
+ }
+
+ @Override
+ public String getSourceFile()
+ {
+ return sourceFile;
+ }
+
+ @Override
+ public void setSourceFile(String newFile)
+ {
+ this.sourceFile = newFile;
+ }
+
+ @Override
+ public boolean isModifiable()
+ {
+ return this.modifiable;
+ }
+
+ /**
+ * Set whether this parameter set is modifiable or not.
+ *
+ * @param modifiable
+ * new modifiable value
+ */
+ public void setModifiable(boolean modifiable)
+ {
+ this.modifiable = modifiable;
+ }
+
+ @Override
+ public List<ArgumentI> getArguments()
+ {
+ return this.arguments;
+ }
+
+ @Override
+ public void setArguments(List<ArgumentI> args)
+ {
+ if (!isModifiable())
+ throw new UnsupportedOperationException(
+ "Attempting to modify an unmodifiable parameter set");
+ this.arguments = Collections.unmodifiableList(new ArrayList<>(args));
+ }
+}