From: Mateusz Warowny Date: Thu, 18 Nov 2021 20:09:36 +0000 (+0100) Subject: JAL-3878 Create backbone for new web services X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=1a3fe80d3170bfa1159673940217cbb56958a555;p=jalview.git JAL-3878 Create backbone for new web services --- diff --git a/src/jalview/util/MathUtils.java b/src/jalview/util/MathUtils.java index ecbb6e1..819d17f 100644 --- a/src/jalview/util/MathUtils.java +++ b/src/jalview/util/MathUtils.java @@ -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 index 0000000..6639012 --- /dev/null +++ b/src/jalview/ws2/WSJob.java @@ -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 index 0000000..9172f7c --- /dev/null +++ b/src/jalview/ws2/WSJobStatus.java @@ -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 index 0000000..0932a87 --- /dev/null +++ b/src/jalview/ws2/WebServiceDiscovererI.java @@ -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 getUrls(); + + /** + * Set the list of urls where this discoverer will search for services. + */ + public void setUrls(List 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 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 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 list); + } + + List 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 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 index 0000000..cdb201e --- /dev/null +++ b/src/jalview/ws2/WebServiceI.java @@ -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 sequences, List 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 index 0000000..fbea582 --- /dev/null +++ b/src/jalview/ws2/gui/MenuEntryProviderI.java @@ -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 index 0000000..36a86e3 --- /dev/null +++ b/src/jalview/ws2/operations/AbstractOperation.java @@ -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 index 0000000..8b432ba --- /dev/null +++ b/src/jalview/ws2/operations/Operation.java @@ -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 index 0000000..450523a --- /dev/null +++ b/src/jalview/ws2/operations/OperationStub.java @@ -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> openEditParamsDialog( + ParamDatastoreI paramStore, WsParamSetI preset, + List 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; + } + }); + } + +}