JAL-3878 Initial preparation.
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Wed, 3 Feb 2021 15:14:50 +0000 (16:14 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 20 Sep 2021 16:43:18 +0000 (18:43 +0200)
src/jalview/util/MathUtils.java
src/jalview/ws2/JalviewWebServiceI.java [new file with mode: 0755]
src/jalview/ws2/MenuEntryProviderI.java [new file with mode: 0755]
src/jalview/ws2/MsaMenuEntryProvider.java [new file with mode: 0755]
src/jalview/ws2/WSJobID.java [new file with mode: 0755]
src/jalview/ws2/WSJobState.java [new file with mode: 0755]
src/jalview/ws2/WSJobTrackerI.java [new file with mode: 0755]
src/jalview/ws2/WebServiceExecutor.java [new file with mode: 0644]
src/jalview/ws2/WebServiceWorkerI.java [new file with mode: 0644]

index ecbb6e1..819d17f 100644 (file)
@@ -39,4 +39,17 @@ public class MathUtils
     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;
+  }
 }
diff --git a/src/jalview/ws2/JalviewWebServiceI.java b/src/jalview/ws2/JalviewWebServiceI.java
new file mode 100755 (executable)
index 0000000..6e65dd0
--- /dev/null
@@ -0,0 +1,67 @@
+package jalview.ws2;
+
+import java.io.IOException;
+import java.util.List;
+
+import jalview.datamodel.SequenceI;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.WsParamSetI;
+
+/**
+ * Provides information about the web service and sub-routines
+ * to submit and track the jobs running on the server as well as
+ * retrieve the results.
+ * The instances should not depend on any other jalview components, especially
+ * must be oblivious to the existence of any UI.
+ * They are used by other classes such as WebServiceWorkers rather than
+ * manipulate data themselves.
+ *
+ * @author mmwarowny
+ *
+ * @param <R>
+ */
+public interface JalviewWebServiceI<R>
+{
+  public static final int PROTEIN_SERVICE = 0x01;
+  public static final int NUCLEOTIDE_SERVICE = 0x02;
+  public static final int ALIGNMENT_ANALYSIS = 0x04;
+  
+  public String getHostName();
+  public String getName();
+  public String getDescription();
+  public String getOperationType();
+  public int getTypeFlags();
+  public boolean canSubmitGaps();
+  public int getMinSequences();
+  public int getMaxSequences();
+  public boolean hasParameters();
+  public ParamDatastoreI getParamStore();
+  
+  public default boolean isProteinService() {
+    return (getTypeFlags() & PROTEIN_SERVICE) > 0;
+  }
+  public default boolean isNucleotideService() {
+    return (getTypeFlags() & NUCLEOTIDE_SERVICE) > 0;
+  }
+  public default boolean isAlignmentAnalysis() {
+    return (getTypeFlags() & ALIGNMENT_ANALYSIS) > 0;
+  }
+  
+  public WSJobID submit(List<SequenceI> sequences, WsParamSetI preset,
+      List<ArgumentI> parameters) throws IOException;
+  
+  public void updateProgress(WSJobID id, WSJobTrackerI tracker)
+      throws IOException;
+  
+  public R getResult(WSJobID id) throws IOException;
+  
+  public void cancel(WSJobID id) throws IOException;
+  
+  public boolean handleSubmissionError(WSJobID id, Throwable th,
+      WSJobTrackerI tracker);
+  
+  public boolean handleCollectionError(WSJobID id, Throwable th,
+      WSJobTrackerI tracker);
+  
+}
diff --git a/src/jalview/ws2/MenuEntryProviderI.java b/src/jalview/ws2/MenuEntryProviderI.java
new file mode 100755 (executable)
index 0000000..ac08c49
--- /dev/null
@@ -0,0 +1,12 @@
+package jalview.ws2;
+
+import javax.swing.JMenu;
+
+import jalview.gui.AlignFrame;
+
+@FunctionalInterface
+public interface MenuEntryProviderI
+{
+  public void buildMenu(JMenu parent, JalviewWebServiceI<?> service,
+      AlignFrame frame);
+}
diff --git a/src/jalview/ws2/MsaMenuEntryProvider.java b/src/jalview/ws2/MsaMenuEntryProvider.java
new file mode 100755 (executable)
index 0000000..d29f638
--- /dev/null
@@ -0,0 +1,211 @@
+package jalview.ws2;
+
+import java.util.List;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Collections;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.ToolTipManager;
+
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.JvSwingUtils;
+import jalview.gui.WsJobParameters;
+import jalview.util.MathUtils;
+import jalview.util.MessageManager;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.WsParamSetI;
+import static java.lang.String.format;
+
+
+class MsaWSWorker implements WebServiceWorkerI
+{
+  private long uid = MathUtils.getUID();
+  
+  JalviewWebServiceI<List<SequenceI>> service;
+
+  private final AlignmentView msa;
+
+  private final AlignmentI seqdataset;
+
+  private boolean submitGaps = false;
+
+  private boolean preserveOrder = false;
+
+  private List<ArgumentI> parameters = Collections.emptyList();
+
+  MsaWSWorker(JalviewWebServiceI<List<SequenceI>> service, AlignmentView msa,
+      boolean submitGaps, boolean preserveOrder, AlignmentI seqdataset)
+  {
+    this.service = service;
+    this.msa = msa;
+    this.seqdataset = seqdataset;
+    this.submitGaps = submitGaps;
+    this.preserveOrder = preserveOrder;
+  }
+  
+  @Override public long getUID() {
+    return uid;
+  };
+
+  void setParameters(List<ArgumentI> parameters)
+  {
+    this.parameters = parameters;
+  }
+  
+  @Override
+  public WSJobID startJob(WSJob job) {
+    return new WSJobID(service.getName(), service.getClass().toString(), "0");
+  }
+  
+  @Override
+  public List<WSJob> getJobs() {
+    return Collections.emptyList();
+  }
+  
+  @Override
+  public boolean pollJob(WSJob job) {
+    return false;
+  }
+
+}
+
+public class MsaMenuEntryProvider implements MenuEntryProviderI
+{
+  WebServiceExecutor executor;
+
+  public MsaMenuEntryProvider(WebServiceExecutor executor)
+  {
+    this.executor = executor;
+  }
+
+  @Override
+  public void buildMenu(JMenu parent, JalviewWebServiceI service, AlignFrame frame)
+  {
+    if (service.canSubmitGaps())
+    {
+      var alignSubmenu = new JMenu(service.getName());
+      buildMenu(alignSubmenu, service, frame, false);
+      parent.add(alignSubmenu);
+      var realignSubmenu = new JMenu(MessageManager.formatMessage(
+          "label.realign_with_params", service.getName()));
+      realignSubmenu.setToolTipText(MessageManager
+          .getString("label.align_sequences_to_existing_alignment"));
+      buildMenu(realignSubmenu, service, frame, true);
+      parent.add(realignSubmenu);
+    }
+    else
+    {
+      buildMenu(parent, service, frame, false);
+    }
+  }
+
+  private void buildMenu(JMenu parent, JalviewWebServiceI<List<SequenceI>> service,
+      AlignFrame frame, boolean submitGaps)
+  {
+    final String action = submitGaps ? "Align" : "Realign";
+    final var calcName = service.getName();
+
+    {
+      var item = new JMenuItem(MessageManager.formatMessage(
+          "label.calcname_with_default_settings", calcName));
+      item.setToolTipText(MessageManager.formatMessage(
+          "label.action_with_default_settings", action));
+      item.addActionListener((event) -> {
+        AlignmentView msa = frame.gatherSequencesForAlignment();
+        AlignmentI dataset = frame.getViewport().getAlignment().getDataset();
+        if (msa != null)
+        {
+          executor.submit(new MsaWSWorker(service, msa, submitGaps, true, dataset));
+        }
+      });
+      parent.add(item);
+    }
+
+    if (service.hasParameters())
+    {
+      var item = new JMenuItem(MessageManager.getString("label.edit_settings_and_run"));
+      item.setToolTipText(MessageManager.getString(
+          "label.view_and_change_parameters_before_alignment"));
+      item.addActionListener((event) -> {
+        AlignmentView msa = frame.gatherSequencesForAlignment();
+        AlignmentI dataset = frame.getViewport().getAlignment().getDataset();
+        if (msa != null)
+        {
+          var parameters = openEditParamsDialog(service, null, null);
+          if (parameters != null)
+          {
+            var thread = new MsaWSWorker(service, msa, submitGaps, true, dataset);
+            thread.setParameters(parameters);
+            executor.submit(thread);
+          }
+        }
+      });
+      parent.add(item);
+    }
+
+    var presets = service.getParamStore().getPresets();
+    if (presets != null && presets.size() > 0)
+    {
+      final var presetList = new JMenu(MessageManager.formatMessage(
+          "label.run_with_preset_params", calcName));
+      final var showToolTipFor = ToolTipManager.sharedInstance().getDismissDelay();
+      for (final var preset : presets)
+      {
+        var item = new JMenuItem(preset.getName());
+        final int QUICK_TOOLTIP = 1500;
+        item.addMouseListener(new MouseAdapter()
+        {
+          @Override public void mouseEntered(MouseEvent e)
+          {
+            ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
+          }
+          @Override public void mouseExited(MouseEvent e)
+          {
+            ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor);
+          }
+        });
+        String tooltip = JvSwingUtils.wrapTooltip(true, format(
+            "<strong>%s</strong><br/>%s", 
+            MessageManager.getString(preset.isModifiable() ? 
+                "label.user_preset" : "label.service_preset"),
+            preset.getDescription()));
+        item.setToolTipText(tooltip);
+        item.addActionListener((event) -> {
+          AlignmentView msa = frame.gatherSequencesForAlignment();
+          AlignmentI dataset = frame.getViewport().getAlignment().getDataset();
+          if (msa != null)
+          {
+            var thread = new MsaWSWorker(service, msa, submitGaps, true, dataset);
+            thread.setParameters(preset.getArguments());
+            executor.submit(thread);
+          }
+        });
+        presetList.add(item);
+      }
+      parent.add(presetList);
+    }
+
+  }
+
+  private List<ArgumentI> openEditParamsDialog(JalviewWebServiceI service,
+      WsParamSetI preset, List<ArgumentI> arguments)
+  {
+    WsJobParameters jobParams;
+    if (preset == null && arguments != null && arguments.size() > 0)
+      jobParams = new WsJobParameters(service.getParamStore(), preset, arguments);
+    else
+      jobParams = new WsJobParameters(service.getParamStore(), preset, null);
+    if (!jobParams.showRunDialog())
+      // cancelled
+      return null;
+    if (jobParams.getPreset() == null)
+      return jobParams.getJobParams();
+    else
+      return jobParams.getPreset().getArguments();
+  }
+}
diff --git a/src/jalview/ws2/WSJobID.java b/src/jalview/ws2/WSJobID.java
new file mode 100755 (executable)
index 0000000..97a1f1a
--- /dev/null
@@ -0,0 +1,69 @@
+package jalview.ws2;
+
+import java.io.Serializable;
+import java.util.Date;
+import static java.lang.String.format;
+
+public final class WSJobID implements Serializable
+{
+  private static final long serialVersionUID = -4600214977954333787L;
+  private String serviceType = "";
+  private String serviceImpl = "";
+  private String jobID = "";
+  private Date creationTime = new Date();
+  
+  public WSJobID() {}
+  
+  public WSJobID(String serviceType, String serviceImpl, String jobID) {
+    this.serviceType = serviceType;
+    this.serviceImpl = serviceImpl;
+    this.jobID = jobID;
+  }
+  
+  @Override
+  public String toString() {
+    return format("%s:%s [%s] Created %s",
+        serviceType, serviceImpl, jobID, creationTime);
+  }
+
+  public String getServiceType()
+  {
+    return serviceType;
+  }
+
+  public void setServiceType(String serviceType)
+  {
+    this.serviceType = serviceType;
+  }
+
+  public String getServiceImpl()
+  {
+    return serviceImpl;
+  }
+
+  public void setServiceImpl(String serviceImpl)
+  {
+    this.serviceImpl = serviceImpl;
+  }
+
+  public String getJobID()
+  {
+    return jobID;
+  }
+
+  public void setJobID(String jobID)
+  {
+    this.jobID = jobID;
+  }
+
+  public Date getCreationTime()
+  {
+    return creationTime;
+  }
+
+  public void setCreationTime(Date creationTime)
+  {
+    this.creationTime = creationTime;
+  }
+  
+}
diff --git a/src/jalview/ws2/WSJobState.java b/src/jalview/ws2/WSJobState.java
new file mode 100755 (executable)
index 0000000..719fdbd
--- /dev/null
@@ -0,0 +1,57 @@
+package jalview.ws2;
+
+
+public enum WSJobState
+{
+  INVALID, READY, SUBMITTED, QUEUED, RUNNING, FINISHED, BROKEN, FAILED,
+  UNKNOWN, SERVER_ERROR, CANCELLED;
+
+  public boolean isSubmitted()
+  {
+    switch (this)
+    {
+    case INVALID:
+    case READY:
+      return false;
+    default:
+      return true;
+    }
+  }
+
+  public boolean isCancelled()
+  {
+    return this == WSJobState.CANCELLED;
+  }
+
+  public boolean isDone()
+  {
+    switch (this)
+    {
+    case INVALID:
+    case READY:
+    case SUBMITTED:
+    case QUEUED:
+    case RUNNING:
+      return false;
+    default:
+      return true;
+    }
+  }
+
+  public boolean isRunning()
+  {
+    switch (this)
+    {
+    case QUEUED:
+    case RUNNING:
+      return true;
+    default:
+      return false;
+    }
+  }
+
+  public boolean isQueued()
+  {
+    return this == WSJobState.SUBMITTED;
+  }
+}
\ No newline at end of file
diff --git a/src/jalview/ws2/WSJobTrackerI.java b/src/jalview/ws2/WSJobTrackerI.java
new file mode 100755 (executable)
index 0000000..671610a
--- /dev/null
@@ -0,0 +1,9 @@
+package jalview.ws2;
+
+public interface WSJobTrackerI {
+  public void setState(WSJobState state);
+  public int getLogSize();
+  public void appendLog(String log);
+  public int getErrorLogSize();
+  public void appendErrorLog(String log);
+}
\ No newline at end of file
diff --git a/src/jalview/ws2/WebServiceExecutor.java b/src/jalview/ws2/WebServiceExecutor.java
new file mode 100644 (file)
index 0000000..8959dd6
--- /dev/null
@@ -0,0 +1,85 @@
+package jalview.ws2;
+
+import java.io.IOException;
+import java.util.EventObject;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import jalview.bin.Cache;
+import jalview.ws2.WebServiceWorkerI.WSJob;
+
+public class WebServiceExecutor
+{
+  private ScheduledExecutorService executor =
+      Executors.newSingleThreadScheduledExecutor();
+  
+  public void submit(final WebServiceWorkerI worker)
+  {
+    for (var job : worker.getJobs()) {
+      executor.submit(() -> submitJob(worker, job));
+      executor.schedule(() -> pollJob(worker, job), 1, TimeUnit.SECONDS);
+    }
+  }
+  
+  private void submitJob(WebServiceWorkerI worker, WSJob job) {
+    try {
+      job.setJobID(worker.startJob(job).getJobID());
+      job.resetAllowedExceptions();
+      executor.schedule(() -> pollJob(worker, job), 1, TimeUnit.SECONDS);
+    }
+    catch (IOException e) {
+      Cache.log.error("Exception occurred during job submission", e);
+      if (!job.deductAllowedExceptions()) {
+        job.setState(WSJobState.SERVER_ERROR);
+      }
+    }
+    if (!job.getState().isSubmitted()) {
+      executor.schedule(() -> submitJob(worker, job), 5, TimeUnit.SECONDS);
+    }
+  }
+  
+  private void pollJob(WebServiceWorkerI worker, WSJob job) {
+    try {
+      worker.pollJob(job);
+      job.resetAllowedExceptions();
+    }
+    catch (IOException e) {
+      Cache.log.error("Exception occurred duringn job pollign", e);
+      if (!job.deductAllowedExceptions()) {
+        job.setState(WSJobState.SERVER_ERROR);
+      }
+    }
+    if (!job.getState().isDone()) {
+      executor.schedule(() -> pollJob(worker, job), 1, TimeUnit.SECONDS);
+    }
+  }
+  
+  public static interface WebServiceThreadListenerI
+  {
+    public void threadSubmitted(WebServiceWorkerI thread);
+    public void threadStarted(WebServiceWorkerI thread);
+    public void stateChanged(WebServiceWorkerI thread, WSJobState oldState,
+        WSJobState newState);
+    public void logAppended(WebServiceWorkerI thread, String text);
+    public void errorLogAppended(WebServiceWorkerI thread, String text);
+    public void cancelled(WebServiceWorkerI thread);
+  }
+  
+  
+  List<WebServiceThreadListenerI> listeners = new CopyOnWriteArrayList<>();
+  
+  public void addServiceListener(WebServiceThreadListenerI listener)
+  {
+    if (!listeners.contains(listener))
+      listeners.add(listener);
+  }
+  
+  public void removeServiceListener(WebServiceThreadListenerI listener)
+  {
+    listeners.remove(listener);
+  }
+}
diff --git a/src/jalview/ws2/WebServiceWorkerI.java b/src/jalview/ws2/WebServiceWorkerI.java
new file mode 100644 (file)
index 0000000..1c40eb8
--- /dev/null
@@ -0,0 +1,73 @@
+package jalview.ws2;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.print.attribute.standard.JobState;
+
+import jalview.util.MathUtils;
+
+public interface WebServiceWorkerI
+{
+  public class WSJob
+  {
+    public final long uid = MathUtils.getUID();
+    
+    protected WSJobState state = WSJobState.UNKNOWN;
+
+    protected String jobID = "";
+
+    protected int jobNum = 0;
+
+    protected int allowedExceptions = 3;
+
+    public long getUID() {
+      return uid;
+    }
+    
+    public WSJobState getState()
+    {
+      return state;
+    }
+
+    public void setState(WSJobState state)
+    {
+      this.state = state;
+    }
+
+    public String getJobID()
+    {
+      return jobID;
+    }
+
+    public void setJobID(String jobID) {
+      this.jobID = jobID;
+    }
+    
+    public int getJobNum()
+    {
+      return jobNum;
+    }
+
+    public int getAllowedExceptions()
+    {
+      return allowedExceptions;
+    }
+    
+    public boolean deductAllowedExceptions() {
+      return allowedExceptions-- > 0;
+    }
+    
+    public void resetAllowedExceptions() {
+      allowedExceptions = 3;
+    }
+  }
+
+  public long getUID();
+
+  public List<WSJob> getJobs();
+
+  public WSJobID startJob(WSJob job) throws IOException;
+
+  public boolean pollJob(WSJob job) throws IOException;
+}