JAL-3878 Create backbone for new web services
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Thu, 18 Nov 2021 20:09:36 +0000 (21:09 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Thu, 18 Nov 2021 20:09:36 +0000 (21:09 +0100)
src/jalview/util/MathUtils.java
src/jalview/ws2/WSJob.java [new file with mode: 0644]
src/jalview/ws2/WSJobStatus.java [new file with mode: 0644]
src/jalview/ws2/WebServiceDiscovererI.java [new file with mode: 0644]
src/jalview/ws2/WebServiceI.java [new file with mode: 0644]
src/jalview/ws2/gui/MenuEntryProviderI.java [new file with mode: 0644]
src/jalview/ws2/operations/AbstractOperation.java [new file with mode: 0644]
src/jalview/ws2/operations/Operation.java [new file with mode: 0644]
src/jalview/ws2/operations/OperationStub.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/WSJob.java b/src/jalview/ws2/WSJob.java
new file mode 100644 (file)
index 0000000..6639012
--- /dev/null
@@ -0,0 +1,183 @@
+package jalview.ws2;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.Date;
+
+import jalview.util.MathUtils;
+
+public class WSJob
+{
+  /* Client-side identifier */
+  public final long uid = MathUtils.getUID();
+
+  private int jobNum = 0;
+
+  /* Shortened server name e.g. "slivka" or "jabaws" */
+  private String serviceProvider = "";
+
+  /* Name of the service e.g. "ClustalW2" */
+  private String serviceName = "";
+
+  /* Server-side identifier */
+  private String jobId = "";
+
+  private WSJobStatus status = WSJobStatus.UNKNOWN;
+
+  private String log = "";
+
+  private String errorLog = "";
+
+  /* Base url of the server associated with the job */
+  private String hostName = "";
+
+  private Date creationTime = new Date();
+  
+  private PropertyChangeSupport pcs = new PropertyChangeSupport(this);
+
+  public WSJob()
+  {
+  }
+
+  public WSJob(String serviceProvider, String serviceName, String hostName)
+  {
+    this.serviceProvider = serviceProvider;
+    this.serviceName = serviceName;
+    this.hostName = hostName;
+  }
+
+  @Override
+  public String toString()
+  {
+    return String.format("%s:%s [%s] Created %s", serviceProvider, serviceName,
+            jobId, creationTime);
+  }
+
+  /**
+   * Get the ordinal numer of the job.
+   * 
+   * @return job number
+   */
+  public int getJobNum()
+  {
+    return jobNum;
+  }
+
+  public void setJobNum(int jobNum)
+  {
+    this.jobNum = jobNum;
+  }
+
+  public WSJobStatus getStatus()
+  {
+    return status;
+  }
+
+  public void setStatus(WSJobStatus status)
+  {
+    var oldStatus = this.status;
+    this.status = status;
+    pcs.firePropertyChange("status", oldStatus, status);
+  }
+
+  public String getLog()
+  {
+    return log;
+  }
+
+  public void setLog(String log)
+  {
+    var oldLog = this.log;
+    this.log = log;
+    pcs.firePropertyChange("log", oldLog, log);
+  }
+
+  public String getErrorLog()
+  {
+    return errorLog;
+  }
+
+  public void setErrorLog(String log)
+  {
+    String oldErrorLog = this.errorLog;
+    this.errorLog = log;
+    pcs.firePropertyChange("errorLog", oldErrorLog, this.errorLog);
+  }
+
+  public long getUid()
+  {
+    return uid;
+  }
+
+  public String getServiceProvider()
+  {
+    return serviceProvider;
+  }
+
+  public void setServiceProvider(String serviceProvider)
+  {
+    this.serviceProvider = serviceProvider;
+  }
+
+  public String getServiceName()
+  {
+    return serviceName;
+  }
+
+  public void setServiceName(String serviceName)
+  {
+    this.serviceName = serviceName;
+  }
+
+  public String getJobId()
+  {
+    return jobId;
+  }
+
+  public void setJobId(String jobID)
+  {
+    this.jobId = jobID;
+  }
+
+  public String getHostName()
+  {
+    return hostName;
+  }
+
+  public void setHostName(String hostName)
+  {
+    this.hostName = hostName;
+  }
+
+  public Date getCreationTime()
+  {
+    return creationTime;
+  }
+
+  public void setCreationTime(Date creationTime)
+  {
+    this.creationTime = creationTime;
+  }
+
+  public void addPropertyChangeListener(PropertyChangeListener listener)
+  {
+    pcs.addPropertyChangeListener(listener);
+  }
+
+  public void addPropertyChangeListener(String propertyName,
+          PropertyChangeListener listener)
+  {
+    pcs.addPropertyChangeListener(propertyName, listener);
+  }
+
+  public void removePropertyChangeListener(PropertyChangeListener listener)
+  {
+    pcs.removePropertyChangeListener(listener);
+  }
+
+  public void removePropertyChagneListener(String propertyName,
+          PropertyChangeListener listener)
+  {
+    pcs.removePropertyChangeListener(propertyName, listener);
+  }
+}
diff --git a/src/jalview/ws2/WSJobStatus.java b/src/jalview/ws2/WSJobStatus.java
new file mode 100644 (file)
index 0000000..9172f7c
--- /dev/null
@@ -0,0 +1,105 @@
+package jalview.ws2;
+
+public enum WSJobStatus
+{
+  /** Job has invalid parameters and cannot be started. */
+  INVALID,
+  /** Job is ready to be submitted. */
+  READY,
+  /** Job has been submitted and awaits processing. */
+  SUBMITTED,
+  /** Job has been queued for execution. */
+  QUEUED,
+  /** Job is running. */
+  RUNNING,
+  /** Job has finished with no errors. */
+  FINISHED, BROKEN,
+  /** Job has finished with errors. */
+  FAILED,
+  /** Job cannot be processed or completed due to server error. */
+  SERVER_ERROR,
+  /** Job has been cancelled. */
+  CANCELLED,
+  /** Status cannot be determined. */
+  UNKNOWN;
+
+  public boolean isSubmitted()
+  {
+    switch (this)
+    {
+    case SUBMITTED:
+    case QUEUED:
+    case RUNNING:
+    case FINISHED:
+    case BROKEN:
+    case FAILED:
+    case CANCELLED:
+      return true;
+    case SERVER_ERROR:
+    case INVALID:
+    case READY:
+    default:
+      return false;
+    }
+  }
+
+  public boolean isCancelled()
+  {
+    return this == WSJobStatus.CANCELLED;
+  }
+
+  public boolean isDone()
+  {
+    switch (this)
+    {
+    case FINISHED:
+    case BROKEN:
+    case FAILED:
+    case SERVER_ERROR:
+    case CANCELLED:
+      return true;
+    case INVALID:
+    case READY:
+    case SUBMITTED:
+    case QUEUED:
+    case RUNNING:
+    default:
+      return false;
+    }
+  }
+
+  public boolean isFailed()
+  {
+    switch (this)
+    {
+    case INVALID:
+    case BROKEN:
+    case FAILED:
+    case SERVER_ERROR:
+      return true;
+    case READY:
+    case SUBMITTED:
+    case QUEUED:
+    case RUNNING:
+    case FINISHED:
+    case CANCELLED:
+    default:
+      return false;
+    }
+  }
+
+  public boolean isRunning()
+  {
+    return this == WSJobStatus.RUNNING;
+  }
+
+  public boolean isQueuing()
+  {
+    return this == WSJobStatus.SUBMITTED || this == WSJobStatus.QUEUED;
+  }
+  
+  public boolean isCompleted()
+  {
+    return this == WSJobStatus.FINISHED;
+  }
+}
\ No newline at end of file
diff --git a/src/jalview/ws2/WebServiceDiscovererI.java b/src/jalview/ws2/WebServiceDiscovererI.java
new file mode 100644 (file)
index 0000000..0932a87
--- /dev/null
@@ -0,0 +1,168 @@
+package jalview.ws2;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import jalview.ws2.operations.Operation;
+
+/**
+ * The discoverer and the supplier of the operations/web services available
+ * on the remote hosts. Each web service client used should have it's
+ * implementation of the discoverer acting as an intermediary between the servers
+ * and jalview application. There is no need for more than one discoverer
+ * per web client per application, therefore singletons can be used.
+ * 
+ * The discoverer stores a list of url endpoints where the services can be
+ * found and builds instances of {@link jalview.ws2.operations.Operation}
+ * with associated implementations of {@link jalview.ws2.WebServiceI}.
+ * 
+ * @author mmwarowny
+ *
+ */
+public interface WebServiceDiscovererI
+{
+  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;
+
+  /**
+   * Get the list of urls that this discoverer will use.
+   */
+  public List<String> getUrls();
+
+  /**
+   * Set the list of urls where this discoverer will search for services.
+   */
+  public void setUrls(List<String> wsUrls);
+
+  /**
+   * Test if the url is a valid url for that service discoverer.
+   */
+  public boolean testUrl(URL url);
+
+  /**
+   * 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 status code for the services availability
+   */
+  public int getStatusForUrl(String url);
+
+  /**
+   * Get the list of operations found on the servers.
+   * 
+   * @return list of operations found
+   */
+  public List<Operation> getOperations();
+
+  /**
+   * @return whether there are services found
+   */
+  public boolean hasServices();
+
+  /**
+   * Check if service discovery is still in progress. List of operations may be
+   * incomplete when the discoverer is running.
+   * 
+   * @return whether the discoverer is running
+   */
+  public boolean isRunning();
+
+  /**
+   * Check if the discoverer is done searching for services. List of operations
+   * should be complete if this methods returns true.
+   * 
+   * @return whether the discoverer finished
+   */
+  public boolean isDone();
+
+  /**
+   * Start the service discovery and return a future which will be set with this
+   * discoverer when the process is completed. This method should be called once
+   * on startup and then every time the urls list is updated.
+   * 
+   * @return future that will be set on discovery completion
+   */
+  public CompletableFuture<WebServiceDiscovererI> startDiscoverer();
+
+  /**
+   * Get the error messages that occurred during service discovery.
+   * 
+   * @return error message
+   */
+  public String getErrorMessages();
+
+  /**
+   * An interface for the listeners observing the changes to the operations
+   * list.
+   * 
+   * @author mmwarowny
+   */
+  @FunctionalInterface
+  static interface OperationsChangeListener
+  {
+    /**
+     * Called whenever the operations list of the observed discoverer changes
+     * with that discoverer as the first argument and current operations list as
+     * the second. Operations 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 operationsChanged(WebServiceDiscovererI discoverer,
+        List<Operation> list);
+  }
+
+  List<OperationsChangeListener> serviceListeners = new CopyOnWriteArrayList<>();
+
+  /**
+   * Add an operations list observer that will be notified of any changes.
+   * 
+   * @param listener
+   *          operations list listener
+   */
+  public default void addOperationsChangeListener(
+      OperationsChangeListener listener)
+  {
+    serviceListeners.add(listener);
+  }
+
+  /**
+   * Remove the listener from the observers list.
+   * 
+   * @param listener
+   *          listener to be removed
+   */
+  public default void removeServiceChangeListener(
+      OperationsChangeListener listener)
+  {
+    serviceListeners.remove(listener);
+  }
+
+  /**
+   * Called whenever the list of operations changes. Notifies all listeners of
+   * the change to the operations list. Typically, should be called with an
+   * empty list at the beginning of the service discovery process and for the
+   * second time with the list of discovered operations after that.
+   * 
+   * @param list
+   *          new list of discovered operations
+   */
+  default void fireOperationsChanged(List<Operation> list)
+  {
+    for (var listener : serviceListeners)
+    {
+      listener.operationsChanged(this, list);
+    }
+  }
+}
diff --git a/src/jalview/ws2/WebServiceI.java b/src/jalview/ws2/WebServiceI.java
new file mode 100644 (file)
index 0000000..cdb201e
--- /dev/null
@@ -0,0 +1,105 @@
+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;
+import jalview.ws2.operations.Operation;
+
+/**
+ * Provides information about the web service and sub-routines to submit, track
+ * and cancel 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
+ */
+public interface WebServiceI
+{
+  /**
+   * Get the hostname/url of the remote server which is supplying the service.
+   * 
+   * @return host name
+   */
+  public String getHostName();
+
+  /**
+   * Get the short name of the service supplier.
+   * 
+   * @return short service supplier name
+   */
+  public String getProviderName();
+
+  /**
+   * Get the name of the service
+   * 
+   * @return service name
+   */
+  String getName();
+
+  /**
+   * Get the description of the service.
+   * 
+   * @return service description
+   */
+  String getDescription();
+
+  /**
+   * Return whether the service provider user-adjustable parameters.
+   * 
+   * @return whether service has parameters
+   */
+  boolean hasParameters();
+
+  /**
+   * Get a {@link ParamDatastoreI} object containing service parameters and
+   * presets.
+   * 
+   * @return service parameters and presets
+   */
+  public ParamDatastoreI getParamStore();
+
+  /**
+   * Submit new job to the service with the supplied input sequences and
+   * arguments. Implementations should perform all data parsing necessary for
+   * the job submission and start a new job on the remote server.
+   * 
+   * @param sequences
+   *          input sequences
+   * @param args
+   *          user provided arguments
+   * @return job id
+   * @throws IOException
+   *           submission failed due to a connection error
+   */
+  public String submit(List<SequenceI> sequences, List<ArgumentI> args)
+      throws IOException;
+
+  /**
+   * Update the progress of the running job according to the state reported by
+   * the server. Implementations should fetch the current job status from the
+   * server and update status and log messages on the provided job object.
+   * 
+   * @param job
+   *          job to update
+   * @throws IOException
+   *           server error occurred
+   */
+  public void updateProgress(WSJob job) throws IOException;
+
+  public void cancel(WSJob job) throws IOException;
+
+  /**
+   * Handle an exception that happened during job submission. If the exception
+   * was handled property by this method, it returns true. Otherwise, returns
+   * false indicating the exception should be handled by the caller.
+   */
+  public boolean handleSubmissionError(WSJob job, Exception ex);
+
+  public boolean handleCollectionError(WSJob job, Exception ex);
+
+}
diff --git a/src/jalview/ws2/gui/MenuEntryProviderI.java b/src/jalview/ws2/gui/MenuEntryProviderI.java
new file mode 100644 (file)
index 0000000..fbea582
--- /dev/null
@@ -0,0 +1,28 @@
+package jalview.ws2.gui;
+
+import javax.swing.JMenu;
+
+import jalview.gui.AlignFrame;
+
+/**
+ * Functional interface provided by {@link jalview.ws2.operations.Operation}
+ * instances to construct the menu entry for the operations. The instances are
+ * passed to the {@link jalview.gui.WebServicesMenuBuilder} and called during
+ * menu construction.
+ * 
+ * @author mmwarowny
+ */
+@FunctionalInterface
+public interface MenuEntryProviderI
+{
+  /**
+   * Build menu entries directly under the given menu. This method is called by
+   * {@link jalview.gui.WebServicesMenuBuilder} during menu construction.
+   * 
+   * @param parent
+   *          parent menu
+   * @param frame
+   *          current alignFrame
+   */
+  public void buildMenu(JMenu parent, AlignFrame frame);
+}
diff --git a/src/jalview/ws2/operations/AbstractOperation.java b/src/jalview/ws2/operations/AbstractOperation.java
new file mode 100644 (file)
index 0000000..36a86e3
--- /dev/null
@@ -0,0 +1,144 @@
+package jalview.ws2.operations;
+
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws2.WebServiceI;
+
+/**
+ * Common base for all operation classes which provides boilerplate
+ * implementation for common {@link Operation} methods.
+ * 
+ * @author mmwarowny
+ *
+ */
+public abstract class AbstractOperation implements Operation
+{
+
+  protected final WebServiceI service;
+
+  protected final String typeName;
+
+  protected boolean interactive = false;
+
+  protected boolean protOperation = true;
+
+  protected boolean nucOperation = true;
+
+  protected boolean alignmentAnalysis = false;
+
+  AbstractOperation(WebServiceI service, String typeName)
+  {
+    this.service = service;
+    this.typeName = typeName;
+  }
+
+  @Override
+  public String getName()
+  {
+    return service.getName();
+  }
+
+  @Override
+  public String getDescription()
+  {
+    return service.getDescription();
+  }
+
+  @Override
+  public String getTypeName()
+  {
+    return typeName;
+  }
+
+  @Override
+  public String getHostName()
+  {
+    return service.getHostName();
+  }
+
+  @Override
+  public boolean hasParameters()
+  {
+    return service.hasParameters();
+  }
+
+  @Override
+  public ParamDatastoreI getParamStore()
+  {
+    return service.getParamStore();
+  }
+
+  @Override
+  public int getMinSequences()
+  {
+    return 1;
+  }
+
+  @Override
+  public int getMaxSequences()
+  {
+    return Integer.MAX_VALUE;
+  }
+
+  @Override
+  public boolean canSubmitGaps()
+  {
+    return false;
+  }
+
+  @Override
+  public boolean isProteinOperation()
+  {
+    return protOperation;
+  }
+
+  public void setProteinOperation(boolean value)
+  {
+    protOperation = value;
+  }
+
+  @Override
+  public boolean isNucleotideOperation()
+  {
+    return nucOperation;
+  }
+
+  public void setNucleotideOperation(boolean value)
+  {
+    nucOperation = value;
+  }
+
+  @Override
+  public boolean isInteractive()
+  {
+    return interactive;
+  }
+
+  public void setInteractive(boolean value)
+  {
+    interactive = value;
+  }
+
+  @Override
+  public boolean isAlignmentAnalysis()
+  {
+    return alignmentAnalysis;
+  }
+
+  public void setAlignmentAnalysis(boolean value)
+  {
+    alignmentAnalysis = value;
+  }
+
+  @Override
+  public boolean getFilterNonStandardSymbols()
+  {
+    return false;
+  }
+
+  @Override
+  public boolean getNeedsAlignedSequences()
+  {
+    return false;
+  }
+
+}
diff --git a/src/jalview/ws2/operations/Operation.java b/src/jalview/ws2/operations/Operation.java
new file mode 100644 (file)
index 0000000..8b432ba
--- /dev/null
@@ -0,0 +1,119 @@
+package jalview.ws2.operations;
+
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws2.gui.MenuEntryProviderI;
+
+/**
+ * Operation represents an action which can be performed with a (web)service or
+ * calculator. Examples of operations are multiple sequence alignment or
+ * sequence annotation. There should be one Operation implementation for each
+ * operation that Jalview can perform on sequences or alignments. The concrete
+ * implementations may be further parameterized to alter the functionality (e.g.
+ * making the operation interactive) or restrict input data (e.g. limit to
+ * proteins only).
+ * 
+ * @author mmwarowny
+ *
+ */
+public interface Operation
+{
+  /**
+   * Get the name of the operation. Typically fetched from the server.
+   * 
+   * @return operation name
+   */
+  public String getName();
+
+  /**
+   * Get the description of the operation. Typically fetched from the server.
+   * 
+   * @return operation description
+   */
+  public String getDescription();
+
+  /**
+   * Get the name of the category that the operation falls into. Used to group
+   * the operations under the web services menu.
+   * 
+   * @return category name
+   */
+  public String getTypeName();
+
+  /**
+   * Get the hostname/url of the server which this operation is delegated to.
+   * Typically fetched from the accompanying web service instance.
+   * 
+   * @return server url
+   */
+  public String getHostName();
+
+  /**
+   * Check if the operation has user-customizable input parameters.
+   * 
+   * @return if has parameters
+   */
+  public boolean hasParameters();
+
+  /**
+   * Returns parameter datastore for this operations containing input parameters
+   * and available presets.
+   * 
+   * @return parameter datastore of the operation
+   */
+  public ParamDatastoreI getParamStore();
+
+  /**
+   * @return minimum accepted number of sequences
+   */
+  public int getMinSequences();
+
+  /**
+   * @return maximum accepted number of sequences
+   */
+  public int getMaxSequences();
+
+  /**
+   * @return whether gaps should be included
+   */
+  public boolean canSubmitGaps();
+
+  /**
+   * @return whether works with protein sequences
+   */
+  public boolean isProteinOperation();
+
+  /**
+   * @return whether works with nucleotide sequences
+   */
+  public boolean isNucleotideOperation();
+
+  /**
+   * @return whether should be run interactively
+   */
+  public boolean isInteractive();
+
+  /**
+   * Get the menu builder for this operation which will be used to construct the
+   * web services menu. The builder will be given the parent menu entry which it
+   * should attach menu items to and the current align frame.
+   * 
+   * @return menu entry builder instance
+   */
+  public MenuEntryProviderI getMenuBuilder();
+
+  /**
+   * @return whether this operation is alignment analysis
+   */
+  public boolean isAlignmentAnalysis();
+
+  /**
+   * @return whether non-standatds symbols should be filtered out
+   */
+  public boolean getFilterNonStandardSymbols();
+
+  /**
+   * @return whether it needs aligned sequences
+   */
+  public boolean getNeedsAlignedSequences();
+
+}
diff --git a/src/jalview/ws2/operations/OperationStub.java b/src/jalview/ws2/operations/OperationStub.java
new file mode 100644 (file)
index 0000000..450523a
--- /dev/null
@@ -0,0 +1,97 @@
+package jalview.ws2.operations;
+
+import java.util.List;
+import java.util.concurrent.CompletionStage;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+
+import jalview.bin.Cache;
+import jalview.gui.AlignFrame;
+import jalview.gui.WsJobParameters;
+import jalview.util.MessageManager;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.WsParamSetI;
+import jalview.ws2.WebServiceI;
+import jalview.ws2.gui.MenuEntryProviderI;
+
+public class OperationStub extends AbstractOperation
+{
+
+  public OperationStub(WebServiceI service, String typeName)
+  {
+    super(service, typeName);
+  }
+
+  @Override
+  public MenuEntryProviderI getMenuBuilder()
+  {
+    return this::buildMenu;
+  }
+
+  public void buildMenu(JMenu parent, AlignFrame frame)
+  {
+    {
+      var item = new JMenuItem(MessageManager.formatMessage(
+          "label.calcname_with_default_settings", getName()));
+      item.addActionListener((event) -> {
+        Cache.log.info(String.format("Starting service %s.", getName()));
+      });
+      parent.add(item);
+    }
+    if (hasParameters())
+    {
+      var item = new JMenuItem(
+          MessageManager.getString("label.edit_settings_and_run"));
+      item.setToolTipText(MessageManager.getString(
+          "label.view_and_change_parameters_before_running_calculation"));
+      item.addActionListener((event) -> {
+        openEditParamsDialog(getParamStore(), null, null)
+            .thenAcceptAsync((arguments) -> {
+              if (arguments != null)
+              {
+                Cache.log.info(String.format("Starting service %s with custom parameters.", getName()));
+              }
+            });
+      });
+      parent.add(item);
+    }
+  }
+
+
+  private CompletionStage<List<ArgumentI>> openEditParamsDialog(
+          ParamDatastoreI paramStore, WsParamSetI preset,
+          List<ArgumentI> arguments)
+  {
+    WsJobParameters jobParams;
+    if (preset == null && arguments != null && arguments.size() > 0)
+      jobParams = new WsJobParameters(paramStore, preset, 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)
+      {
+        if (jobParams.getPreset() == null)
+        {
+          return jobParams.getJobParams();
+        }
+        else
+        {
+          return jobParams.getPreset().getArguments();
+        }
+      }
+      else
+      {
+        return null;
+      }
+    });
+  }
+  
+}