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.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);
+ }
+}
--- /dev/null
+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
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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);
+
+}
--- /dev/null
+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);
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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();
+
+}
--- /dev/null
+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;
+ }
+ });
+ }
+
+}