From: Mateusz Warowny Date: Tue, 18 Jul 2023 14:46:30 +0000 (+0200) Subject: Merge branch 'mmw/JAL-4199-task-execution-update' into development/Release_2_12_Branch X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=0760d02366b4e570a1373287438b6e87f3619aea;hp=80d446099e8d1a83a73e64870c8e08903a003b00;p=jalview.git Merge branch 'mmw/JAL-4199-task-execution-update' into development/Release_2_12_Branch --- diff --git a/j11lib/hmmer-client-1.0-SNAPSHOT.jar b/j11lib/hmmer-client-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..1efea10 Binary files /dev/null and b/j11lib/hmmer-client-1.0-SNAPSHOT.jar differ diff --git a/j11lib/simple-http-client-1.0-SNAPSHOT.jar b/j11lib/simple-http-client-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..43b5933 Binary files /dev/null and b/j11lib/simple-http-client-1.0-SNAPSHOT.jar differ diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index f8c2b68..59c2bd3 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -1109,6 +1109,7 @@ info.alignment_object_method_notes = \nAlignment Object Method Notes\n info.server_exception = \n{0} Server exception\!\n{1} info.invalid_msa_input_mininfo = Need at least two sequences with at least 3 residues each, with no hidden regions between them. info.invalid_msa_notenough = Not enough sequence data to align +info.invalid_search_input = Invalid input for sequence search. status.processing_commandline_args = Processing commandline arguments... status.das_features_being_retrived = DAS features being retrieved... status.searching_for_sequences_from = Searching for sequences from {0} diff --git a/src/jalview/api/AlignCalcWorkerI.java b/src/jalview/api/AlignCalcWorkerI.java index b51b94e..89470e0 100644 --- a/src/jalview/api/AlignCalcWorkerI.java +++ b/src/jalview/api/AlignCalcWorkerI.java @@ -65,4 +65,12 @@ public interface AlignCalcWorkerI * @return */ boolean isDeletable(); + + /** + * Returns the name of this calculation. + */ + public default String getCalcName() + { + return null; + } } diff --git a/src/jalview/datamodel/AnnotatedCollectionI.java b/src/jalview/datamodel/AnnotatedCollectionI.java index 878f22a..755333c 100644 --- a/src/jalview/datamodel/AnnotatedCollectionI.java +++ b/src/jalview/datamodel/AnnotatedCollectionI.java @@ -62,5 +62,21 @@ public interface AnnotatedCollectionI extends SequenceCollectionI */ AnnotatedCollectionI getContext(); - + /** + * Returns the number of the highest annotation graph group attached to this + * alignment. + */ + default int getLastGraphGroup() + { + var annots = getAlignmentAnnotation(); + if (annots == null) + return 1; + int graphGroup = 1; + for (AlignmentAnnotation ala : annots) + { + graphGroup = Math.max(graphGroup, ala.graphGroup); + } + return graphGroup; + } + } diff --git a/src/jalview/datamodel/ContiguousI.java b/src/jalview/datamodel/ContiguousI.java index a9b1372..bc72984 100644 --- a/src/jalview/datamodel/ContiguousI.java +++ b/src/jalview/datamodel/ContiguousI.java @@ -20,9 +20,23 @@ */ package jalview.datamodel; +import java.util.Collection; + public interface ContiguousI { int getBegin(); // todo want long for genomic positions? int getEnd(); + + public static int[] toStartEndArray(Collection ranges) + { + int[] startend = new int[ranges.size() * 2]; + int i = 0; + for (var range : ranges) + { + startend[i++] = range.getBegin(); + startend[i++] = range.getEnd(); + } + return startend; + } } diff --git a/src/jalview/datamodel/SequenceI.java b/src/jalview/datamodel/SequenceI.java index 8f1d160..d8a5462 100755 --- a/src/jalview/datamodel/SequenceI.java +++ b/src/jalview/datamodel/SequenceI.java @@ -396,6 +396,19 @@ public interface SequenceI extends ASequenceI public SequenceI getDatasetSequence(); /** + * Returns the top grandparent in the dataset sequences hierarchy. + */ + public default SequenceI getRootDatasetSequence() + { + var sequence = this; + while (sequence.getDatasetSequence() != null) + { + sequence = sequence.getDatasetSequence(); + } + return sequence; + } + + /** * Returns a new array containing this sequence's annotations, or null. */ public AlignmentAnnotation[] getAnnotation(); diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index 63bbb72..8ae4601 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -183,6 +183,7 @@ import jalview.ws.params.ParamDatastoreI; import jalview.ws.params.WsParamSetI; import jalview.ws.seqfetcher.DbSourceProxy; import jalview.ws2.client.api.WebServiceDiscovererI; +import jalview.ws2.client.ebi.JobDispatcherWSDiscoverer; import jalview.ws2.client.slivka.SlivkaWSDiscoverer; import jalview.ws2.gui.WebServicesMenuManager; @@ -977,6 +978,15 @@ public class AlignFrame extends GAlignFrame menu.setNoServices(services.isEmpty() && discoverer.isDone()); }; + private WebServiceDiscovererI.ServicesChangeListener ebiServiceChangeListener = + (discoverer, services) -> { + // run when ebi services change + var menu = AlignFrame.this.ebiMenu; + menu.setServices(discoverer); + menu.setInProgress(discoverer.isRunning()); + menu.setNoServices(services.isEmpty() && discoverer.isDone()); + }; + /* Set up intrinsic listeners for dynamically generated GUI bits. */ private void addServiceListeners() { @@ -985,6 +995,10 @@ public class AlignFrame extends GAlignFrame WebServiceDiscovererI discoverer = SlivkaWSDiscoverer.getInstance(); discoverer.addServicesChangeListener(slivkaServiceChangeListener); } + if (Cache.getDefault("SHOW_EBI_SERVICES", true)) + { + JobDispatcherWSDiscoverer.getInstance().addServicesChangeListener(ebiServiceChangeListener); + } if (Cache.getDefault("SHOW_JWS2_SERVICES", true)) { WSDiscovererI discoverer = Jws2Discoverer.getInstance(); @@ -4657,6 +4671,7 @@ public class AlignFrame extends GAlignFrame } private WebServicesMenuManager slivkaMenu = new WebServicesMenuManager("slivka", this); + private WebServicesMenuManager ebiMenu = new WebServicesMenuManager("job dispatcher", this); /** * Schedule the web services menu rebuild to the event dispatch thread. @@ -4675,6 +4690,15 @@ public class AlignFrame extends GAlignFrame slivkaMenu.setNoServices(discoverer.isDone() && !discoverer.hasServices()); webService.add(slivkaMenu.getMenu()); } + if (Cache.getDefault("SHOW_EBI_SERVICES", true)) + { + Console.info("Building web services menu for jobs dispatcher"); + JobDispatcherWSDiscoverer discoverer = JobDispatcherWSDiscoverer.getInstance(); + ebiMenu.setServices(discoverer); + ebiMenu.setInProgress(discoverer.isRunning()); + ebiMenu.setNoServices(discoverer.isDone() && !discoverer.hasServices()); + webService.add(ebiMenu.getMenu()); + } if (Cache.getDefault("SHOW_JWS2_SERVICES", true)) { WSDiscovererI jws2servs = Jws2Discoverer.getInstance(); diff --git a/src/jalview/gui/Desktop.java b/src/jalview/gui/Desktop.java index 0b639dd..02643ec 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -2742,6 +2742,11 @@ public class Desktop extends GDesktop tasks.add(jalview.ws2.client.slivka.SlivkaWSDiscoverer .getInstance().startDiscoverer()); } + if (Cache.getDefault("SHOW_EBI_SERVICES", true)) + { + tasks.add(jalview.ws2.client.ebi.JobDispatcherWSDiscoverer + .getInstance().startDiscoverer()); + } if (blocking) { for (Future task : tasks) { diff --git a/src/jalview/ws/params/simple/BooleanOption.java b/src/jalview/ws/params/simple/BooleanOption.java index 87e4ad1..8609ff0 100644 --- a/src/jalview/ws/params/simple/BooleanOption.java +++ b/src/jalview/ws/params/simple/BooleanOption.java @@ -24,6 +24,8 @@ import java.net.URL; import java.util.Arrays; import java.util.List; +import jalview.ws.params.ArgumentI; + public class BooleanOption extends Option { public static class Builder extends Option.Builder @@ -98,4 +100,10 @@ public class BooleanOption extends Option { this(name, description, label, isrequired, defValue, String.valueOf(true), link); } + + public static Boolean parseBoolean(ArgumentI argument) + { + return argument.getValue() != null && !argument.getValue().isEmpty() ? + true : false; + } } diff --git a/src/jalview/ws/params/simple/DoubleParameter.java b/src/jalview/ws/params/simple/DoubleParameter.java index 97c5fe1..f08f5fd 100644 --- a/src/jalview/ws/params/simple/DoubleParameter.java +++ b/src/jalview/ws/params/simple/DoubleParameter.java @@ -1,5 +1,6 @@ package jalview.ws.params.simple; +import jalview.ws.params.ArgumentI; import jalview.ws.params.ParameterI; import jalview.ws.params.ValueConstrainI; @@ -152,4 +153,28 @@ public class DoubleParameter extends Option implements ParameterI { return new DoubleParameter(this); } + + /** + * Return argument value as double or null if string value is null or empty. + * + * @param arg argument to extract value form + * @return argument value as double + */ + public static Double parseDouble(ArgumentI arg) + { + return arg.getValue() != null && !arg.getValue().isEmpty() ? + Double.parseDouble(arg.getValue()) : null; + } + + /** + * Return argument value as float or null if string value is null or empty. + * + * @param arg argument to extract value from + * @return value as float + */ + public static Float parseFloat(ArgumentI arg) + { + return arg.getValue() != null && !arg.getValue().isEmpty() ? + Float.parseFloat(arg.getValue()) : null; + } } diff --git a/src/jalview/ws/params/simple/IntegerParameter.java b/src/jalview/ws/params/simple/IntegerParameter.java index e154194..774c21f 100644 --- a/src/jalview/ws/params/simple/IntegerParameter.java +++ b/src/jalview/ws/params/simple/IntegerParameter.java @@ -20,6 +20,7 @@ */ package jalview.ws.params.simple; +import jalview.ws.params.ArgumentI; import jalview.ws.params.ParameterI; import jalview.ws.params.ValueConstrainI; @@ -165,4 +166,16 @@ public class IntegerParameter extends Option implements ParameterI return new IntegerParameter(this); } + /** + * Return argument value as int or null if string value is null or empty. + * + * @param arg argument to extract value from + * @return value as int + */ + public static Integer parseInt(ArgumentI arg) + { + return arg.getValue() != null && !arg.getValue().isEmpty() ? + Integer.parseInt(arg.getValue()) : null; + } + } diff --git a/src/jalview/ws2/actions/AbstractPollableTask.java b/src/jalview/ws2/actions/AbstractPollableTask.java deleted file mode 100644 index b61711c..0000000 --- a/src/jalview/ws2/actions/AbstractPollableTask.java +++ /dev/null @@ -1,352 +0,0 @@ -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.bin.Console; -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 - * the type of jobs managed by the task - * @param - * the type of result provided by the task - */ -public abstract class AbstractPollableTask implements TaskI -{ - private final long uid = MathUtils.getUID(); - - protected final WebServiceClientI client; - - protected final List args; - - protected final Credentials credentials; - - private final TaskEventSupport eventHandler; - - protected JobStatus taskStatus = JobStatus.CREATED; - - private Future future = null; - - protected List jobs = Collections.emptyList(); - - protected R result; - - protected AbstractPollableTask(WebServiceClientI client, List args, - Credentials credentials, TaskEventListener eventListener) - { - this.client = client; - this.args = args; - this.credentials = credentials; - this.eventHandler = new TaskEventSupport(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: - *
    - *
  • task is invalid if all jobs are invalid
  • - *
  • task is completed if all but invalid jobs are completed
  • - *
  • 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.
  • - *
  • task is running if at least one job is running and none are failed or - * cancelled
  • - *
  • task is cancelled if at least one job is cancelled and none failed
  • - *
  • task is failed or server error if at least one job is failed or server - * error
  • - *
- */ - 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 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 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) - { - Console.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); - } -} diff --git a/src/jalview/ws2/actions/BaseJob.java b/src/jalview/ws2/actions/BaseJob.java index 8376d20..79c1aa8 100644 --- a/src/jalview/ws2/actions/BaseJob.java +++ b/src/jalview/ws2/actions/BaseJob.java @@ -25,6 +25,7 @@ import jalview.ws2.api.WebServiceJobHandle; * * @author mmwarowny */ +// TODO: make class non-abstract by removing isInputValid() public abstract class BaseJob implements JobI { protected final long internalId = MathUtils.getUID(); @@ -70,6 +71,7 @@ public abstract class BaseJob implements JobI * * @return {@code true} if the input is valid. */ + // FIXME: method not necessary, may incorporate into task#prepare() public abstract boolean isInputValid(); /** @@ -175,7 +177,7 @@ public abstract class BaseJob implements JobI * @param listener * property change listener */ - public final void addPropertyChagneListener(PropertyChangeListener listener) + public final void addPropertyChangeListener(PropertyChangeListener listener) { pcs.addPropertyChangeListener(listener); } diff --git a/src/jalview/ws2/actions/BaseTask.java b/src/jalview/ws2/actions/BaseTask.java new file mode 100644 index 0000000..387ca69 --- /dev/null +++ b/src/jalview/ws2/actions/BaseTask.java @@ -0,0 +1,311 @@ +package jalview.ws2.actions; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import jalview.bin.Console; +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.client.api.WebServiceClientI; +import jalview.ws2.helpers.DelegateJobEventListener; +import jalview.ws2.helpers.TaskEventSupport; + +import static java.lang.String.format; + +public abstract class BaseTask implements TaskI +{ + protected final long uid = MathUtils.getUID(); + + protected final WebServiceClientI webClient; + + protected final List args; + + protected final Credentials credentials; + + private final TaskEventSupport eventHandler; + + protected JobStatus status = JobStatus.CREATED; + + protected List jobs = Collections.emptyList(); + + protected R result = null; + + protected Runnable cancelAction = () -> { + }; + + protected BaseTask(WebServiceClientI webClient, List args, + Credentials credentials) + { + this.webClient = webClient; + this.args = args; + this.credentials = credentials; + this.eventHandler = new TaskEventSupport<>(this); + } + + @Override + public final long getUid() + { + return uid; + } + + @Override + public final JobStatus getStatus() + { + return status; + } + + @Override + public final List getSubJobs() + { + return jobs; + } + + @Override + public final void addTaskEventListener(TaskEventListener listener) + { + eventHandler.addListener(listener); + } + + @Override + public final void removeTaskEventListener(TaskEventListener listener) + { + eventHandler.addListener(listener); + } + + @Override + public final R getResult() + { + return result; + } + + @Override + public final void init() throws Exception + { + try + { + jobs = prepareJobs(); + } catch (ServiceInputInvalidException e) + { + setStatus(JobStatus.INVALID); + eventHandler.fireTaskException(e); + throw e; + } + setStatus(JobStatus.READY); + eventHandler.fireTaskStarted(jobs); + var jobListener = new DelegateJobEventListener<>(eventHandler); + for (var job : jobs) + job.addPropertyChangeListener(jobListener); + submitJobs(jobs); + } + + static final int MAX_SUBMIT_RETRY = 5; + + protected final void submitJobs(List jobs) throws IOException + { + var retryCounter = 0; + while (true) + { + try + { + submitJobs0(jobs); + setStatus(JobStatus.SUBMITTED); + break; + } catch (IOException e) + { + eventHandler.fireTaskException(e); + if (++retryCounter > MAX_SUBMIT_RETRY) + { + cancel(); + setStatus(JobStatus.SERVER_ERROR); + throw e; + } + } + } + } + + private final void submitJobs0(List jobs) throws IOException + { + IOException exception = null; + for (BaseJob job : jobs) + { + if (job.getStatus() != JobStatus.READY || !job.isInputValid()) + continue; + try + { + var jobRef = webClient.submit(job.getInputSequences(), args, credentials); + job.setServerJob(jobRef); + job.setStatus(JobStatus.SUBMITTED); + } catch (IOException e) + { + exception = e; + } + } + if (exception != null) + throw exception; + } + + /** + * 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 + */ + @Override + public final boolean poll() throws IOException + { + boolean allDone = true; + IOException exception = null; + for (BaseJob job : jobs) + { + if (job.isInputValid() && !job.getStatus().isDone()) + { + var serverJob = job.getServerJob(); + try + { + job.setStatus(webClient.getStatus(serverJob)); + job.setLog(webClient.getLog(serverJob)); + job.setErrorLog(webClient.getErrorLog(serverJob)); + } catch (IOException e) + { + exception = e; + } + } + allDone &= job.isCompleted(); + } + updateGlobalStatus(); + if (exception != null) + throw exception; + return allDone; + } + + @Override + public final void complete() throws IOException + { + for (var job : jobs) + { + if (!job.isCompleted()) + { + // a fallback in case the executor decides to finish prematurely + cancelJob(job); + job.setStatus(JobStatus.SERVER_ERROR); + } + } + updateGlobalStatus(); + try { + result = collectResult(jobs); + eventHandler.fireTaskCompleted(result); + } + catch (Exception e) + { + eventHandler.fireTaskException(e); + throw e; + } + } + + /** + * Cancel all running jobs. Used in case of task failure to cleanup the + * resources or when the task has been cancelled. + */ + @Override + public final void cancel() + { + cancelAction.run(); + for (T job : jobs) + { + cancelJob(job); + } + setStatus(JobStatus.CANCELLED); + } + + private final void cancelJob(T job) + { + if (!job.isCompleted()) + { + try + { + if (job.getServerJob() != null) + webClient.cancel(job.getServerJob()); + job.setStatus(JobStatus.CANCELLED); + } catch (IOException e) + { + Console.error(format("failed to cancel job %s", job.getServerJob()), e); + } + } + } + + protected final void setStatus(JobStatus status) + { + Objects.requireNonNull(status); + if (this.status != status) + { + this.status = status; + eventHandler.fireTaskStatusChanged(status); + } + } + + protected abstract List prepareJobs() throws ServiceInputInvalidException; + + protected abstract R collectResult(List jobs) throws IOException; + + /** + * Update task status according to the overall status of its jobs. The rules + * of setting the status are following: + *
    + *
  • task is invalid if all jobs are invalid
  • + *
  • task is completed if all but invalid jobs are completed
  • + *
  • 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.
  • + *
  • task is running if at least one job is running and none are failed or + * cancelled
  • + *
  • task is cancelled if at least one job is cancelled and none failed
  • + *
  • task is failed or server error if at least one job is failed or server + * error
  • + *
+ */ + protected final 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]); + } + } + + /** + * Set the action that will be run when the {@link #cancel()} method is + * invoked. The action should typically stop the executor polling the task and + * release resources and threads running the task. + * + * @param action + * runnable to be executed when the task is cancelled + */ + public void setCancelAction(Runnable action) + { + Objects.requireNonNull(action); + this.cancelAction = action; + } + + @Override + public String toString() + { + var statusName = status != null ? status.name() : "UNSET"; + return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, statusName); + } +} diff --git a/src/jalview/ws2/actions/NullAction.java b/src/jalview/ws2/actions/NullAction.java new file mode 100644 index 0000000..f91cef6 --- /dev/null +++ b/src/jalview/ws2/actions/NullAction.java @@ -0,0 +1,51 @@ +package jalview.ws2.actions; + +import java.util.List; + +import jalview.api.AlignViewportI; +import jalview.viewmodel.AlignmentViewport; +import jalview.ws.params.ArgumentI; +import jalview.ws2.actions.api.ActionI; +import jalview.ws2.actions.api.TaskI; +import jalview.ws2.api.Credentials; + +/** + * An empty implementation of the {@link ActionI} interface that does nothing. + * Use as a placeholder for testing purposes. + * + * @author mmwarowny + * + */ +public final class NullAction extends BaseAction +{ + public static final class Builder extends BaseAction.Builder + { + public NullAction build() + { + return new NullAction(this); + } + } + + public static Builder newBuilder() + { + return new Builder(); + } + + protected NullAction(Builder builder) + { + super(builder); + } + + @Override + public TaskI createTask(AlignViewportI viewport, + List args, Credentials credentials) + { + return new NullTask(); + } + + @Override + public boolean isActive(AlignmentViewport viewport) + { + return false; + } +} diff --git a/src/jalview/ws2/actions/NullTask.java b/src/jalview/ws2/actions/NullTask.java new file mode 100644 index 0000000..5dd5ab0 --- /dev/null +++ b/src/jalview/ws2/actions/NullTask.java @@ -0,0 +1,74 @@ +package jalview.ws2.actions; + +import java.util.Collections; +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; + +/** + * An empty task returned by the {@link NullAction}. Use as a placeholder for + * testing purposes. + * + * @author mmwarowny + * + */ +class NullTask implements TaskI +{ + @Override + public long getUid() + { + return 0; + } + + @Override + public JobStatus getStatus() + { + return JobStatus.READY; + } + + @Override + public List getSubJobs() + { + return Collections.emptyList(); + } + + @Override + public void init() throws Exception + { + } + + @Override + public boolean poll() throws Exception + { + return true; + } + + @Override + public void complete() throws Exception + { + } + + @Override + public Void getResult() + { + return null; + } + + @Override + public void cancel() + { + } + + @Override + public void addTaskEventListener(TaskEventListener listener) + { + } + + @Override + public void removeTaskEventListener(TaskEventListener listener) + { + } +} \ No newline at end of file diff --git a/src/jalview/ws2/actions/PollingTaskExecutor.java b/src/jalview/ws2/actions/PollingTaskExecutor.java new file mode 100644 index 0000000..ff7a3db --- /dev/null +++ b/src/jalview/ws2/actions/PollingTaskExecutor.java @@ -0,0 +1,108 @@ +package jalview.ws2.actions; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; +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.ws2.actions.api.TaskI; + +public class PollingTaskExecutor +{ + private static final Map executorPool = + Collections.synchronizedMap(new WeakHashMap<>()); + + public static PollingTaskExecutor fromPool(ScheduledExecutorService executor) + { + return executorPool.computeIfAbsent(executor, PollingTaskExecutor::new); + } + + private final ScheduledExecutorService executor; + + public PollingTaskExecutor(ScheduledExecutorService executor) + { + this.executor = executor; + } + + public Future submit(TaskI task) + { + Objects.requireNonNull(task); + return executor.scheduleWithFixedDelay( + new TaskRunnable(task), 0, 2, TimeUnit.SECONDS); + } + + private static class TaskRunnable implements Runnable + { + private final TaskI task; + + private static final int STAGE_INIT = 0; + + private static final int STAGE_POLLING = 2; + + private static final int STAGE_FINISHED = 3; + + private static final int STAGE_STOPPED = 4; + + private int stage = STAGE_INIT; + + private static final int MAX_POLL_RETRY = 5; + + private int pollRetryCount = 0; + + private TaskRunnable(TaskI task) + { + this.task = task; + } + + @Override + public void run() + { + if (task.getStatus().isDone()) + { + stage = STAGE_STOPPED; + } + if (stage == STAGE_INIT) + { + try + { + task.init(); + stage = STAGE_POLLING; + } catch (Exception e) + { + stage = STAGE_STOPPED; + throw new CompletionException(e); + } + } + try + { + if (stage == STAGE_POLLING && task.poll()) + { + stage = STAGE_FINISHED; + } + if (stage == STAGE_FINISHED) + { + task.complete(); + stage = STAGE_STOPPED; + } + } catch (Exception e) + { + if (++pollRetryCount > MAX_POLL_RETRY || e instanceof RuntimeException) + { + task.cancel(); + stage = STAGE_STOPPED; + throw new CompletionException(e); + } + } + if (stage == STAGE_STOPPED) + { + throw new CancellationException(); + } + } + } +} diff --git a/src/jalview/ws2/actions/alignment/AlignmentAction.java b/src/jalview/ws2/actions/alignment/AlignmentAction.java index 7f935bf..ce4d499 100644 --- a/src/jalview/ws2/actions/alignment/AlignmentAction.java +++ b/src/jalview/ws2/actions/alignment/AlignmentAction.java @@ -3,9 +3,12 @@ package jalview.ws2.actions.alignment; import java.util.List; import java.util.Objects; +import jalview.api.AlignViewportI; import jalview.viewmodel.AlignmentViewport; import jalview.ws.params.ArgumentI; import jalview.ws2.actions.BaseAction; +import jalview.ws2.actions.BaseTask; +import jalview.ws2.actions.PollingTaskExecutor; import jalview.ws2.actions.api.TaskEventListener; import jalview.ws2.actions.api.TaskI; import jalview.ws2.api.Credentials; @@ -68,15 +71,11 @@ public class AlignmentAction extends BaseAction } @Override - public TaskI perform(AlignmentViewport viewport, - List args, Credentials credentials, - TaskEventListener handler) + public AlignmentTask createTask(AlignViewportI viewport, + List args, Credentials credentials) { - var msa = viewport.getAlignmentView(true); - var task = new AlignmentTask( - client, this, args, credentials, msa, viewport, submitGaps, handler); - task.start(viewport.getServiceExecutor()); - return task; + return new AlignmentTask( + client, this, args, credentials, viewport, submitGaps); } /** diff --git a/src/jalview/ws2/actions/alignment/AlignmentTask.java b/src/jalview/ws2/actions/alignment/AlignmentTask.java index 6a0c4dd..944e5dd 100644 --- a/src/jalview/ws2/actions/alignment/AlignmentTask.java +++ b/src/jalview/ws2/actions/alignment/AlignmentTask.java @@ -5,7 +5,6 @@ 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; @@ -14,7 +13,6 @@ import jalview.analysis.AlignmentSorter; import jalview.analysis.SeqsetUtils; import jalview.analysis.SeqsetUtils.SequenceInfo; import jalview.api.AlignViewportI; -import jalview.bin.Cache; import jalview.bin.Console; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.Alignment; @@ -25,9 +23,8 @@ 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.BaseTask; 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; @@ -39,7 +36,7 @@ import jalview.ws2.client.api.AlignmentWebServiceClientI; * @author mmwarowny * */ -class AlignmentTask extends AbstractPollableTask +class AlignmentTask extends BaseTask { /* task parameters set in the constructor */ private final AlignmentWebServiceClientI client; @@ -48,8 +45,6 @@ class AlignmentTask extends AbstractPollableTask private final AlignmentView msa; // a.k.a. input - private final AlignViewportI viewport; - private final boolean submitGaps; private final AlignmentI currentView; @@ -62,14 +57,12 @@ class AlignmentTask extends AbstractPollableTask AlignmentTask(AlignmentWebServiceClientI client, AlignmentAction action, List args, Credentials credentials, - AlignmentView msa, AlignViewportI viewport, boolean submitGaps, - TaskEventListener eventListener) + AlignViewportI viewport, boolean submitGaps) { - super(client, args, credentials, eventListener); + super(client, args, credentials); this.client = client; this.action = action; - this.msa = msa; - this.viewport = viewport; + this.msa = viewport.getAlignmentView(true); this.submitGaps = submitGaps; this.currentView = viewport.getAlignment(); this.dataset = viewport.getAlignment().getDataset(); @@ -80,7 +73,7 @@ class AlignmentTask extends AbstractPollableTask } @Override - protected List prepare() throws ServiceInputInvalidException + protected List prepareJobs() throws ServiceInputInvalidException { Console.info(format("starting alignment service %s:%s", client.getClientName(), action.getName())); @@ -107,7 +100,7 @@ class AlignmentTask extends AbstractPollableTask } @Override - protected AlignmentResult done() throws IOException + protected AlignmentResult collectResult(List jobs) throws IOException { IOException lastIOE = null; for (AlignmentJob job : jobs) diff --git a/src/jalview/ws2/actions/annotation/AlignCalcWorkerAdapter.java b/src/jalview/ws2/actions/annotation/AlignCalcWorkerAdapter.java new file mode 100644 index 0000000..0905ea2 --- /dev/null +++ b/src/jalview/ws2/actions/annotation/AlignCalcWorkerAdapter.java @@ -0,0 +1,188 @@ +package jalview.ws2.actions.annotation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +import jalview.analysis.AlignmentAnnotationUtils; +import jalview.api.AlignViewportI; +import jalview.api.AlignmentViewPanel; +import jalview.api.PollableAlignCalcWorkerI; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.AlignmentI; +import jalview.workers.AlignCalcWorker; +import jalview.ws.params.ArgumentI; +import jalview.ws2.actions.api.TaskEventListener; +import jalview.ws2.actions.api.TaskI; +import jalview.ws2.api.Credentials; + +public class AlignCalcWorkerAdapter extends AlignCalcWorker implements PollableAlignCalcWorkerI +{ + private static int calcCount = 0; + + private final int calcNumber = calcCount++; + + private final AnnotationAction action; + + private final List args; + + private final Credentials credentials; + + private TaskI currentTask = null; + + private TaskEventListener taskListener = new TaskEventListener<>() + { + @Override + public void taskCompleted(TaskI source, AnnotationResult result) + { + int graphGroup = alignViewport.getAlignment().getLastGraphGroup(); + List annotations = new ArrayList<>(); + for (AlignmentAnnotation ala : result.getAnnotations()) + { + if (ala.graphGroup > 0) + ala.graphGroup += graphGroup; + var newAnnot = alignViewport.getAlignment() + .updateFromOrCopyAnnotation(ala); + if (ala.sequenceRef != null) + { + ala.sequenceRef.addAlignmentAnnotation(newAnnot); + newAnnot.adjustForAlignment(); + AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith( + newAnnot, newAnnot.label, newAnnot.getCalcId()); + } + annotations.add(newAnnot); + } + updateOurAnnots(annotations); + listener.workerHasResult( + AlignCalcWorkerAdapter.this, + new AnnotationResult( + annotations, + result.transferFeatures, + result.featureColours, + result.featureFilters)); + } + }; + + public AlignCalcWorkerAdapter(AlignViewportI alignViewport, AlignmentViewPanel alignPanel, AnnotationAction action, + List args, Credentials credentials) + { + super(alignViewport, alignPanel); + this.action = action; + this.args = args; + this.credentials = credentials; + } + + @Override + public String getCalcName() + { + return action.getWebService().getName(); + } + + @Override + public void updateAnnotation() + { + + } + + private void updateOurAnnots(List newAnnots) + { + List oldAnnots = ourAnnots != null ? ourAnnots : List.of(); + AlignmentI alignment = alignViewport.getAlignment(); + for (AlignmentAnnotation annotation : oldAnnots) + if (!newAnnots.contains(annotation)) + alignment.deleteAnnotation(annotation); + for (AlignmentAnnotation annotation : newAnnots) + alignment.validateAnnotation(annotation); + ourAnnots = Collections.synchronizedList(newAnnots); + } + + @Override + public synchronized void startUp() throws Throwable + { + if (alignViewport.isClosed()) + { + calcMan.disableWorker(this); + abortAndDestroy(); + throw new IllegalStateException("Starting calculation for closed viewport"); + } + currentTask = action.createTask(alignViewport, args, credentials); + currentTask.addTaskEventListener(taskListener); + currentTask.init(); + listener.workerStarted(this); + } + + @Override + public boolean poll() throws Throwable + { + return currentTask.poll(); + } + + @Override + public synchronized void cancel() + { + try + { + currentTask.cancel(); + } finally + { + currentTask.removeTaskEventListener(taskListener); + listener.workerStopped(this); + } + } + + @Override + public synchronized void done() + { + try + { + currentTask.complete(); + } catch (Exception e) + { + } finally + { + currentTask.removeTaskEventListener(taskListener); + listener.workerStopped(this); + } + } + + @Override + public boolean isDeletable() + { + return true; + } + + @Override + public String toString() + { + return "AlignCalcWorkerAdapter-" + calcNumber + " for " + getCalcName(); + } + + public interface WorkerListener extends java.util.EventListener + { + default void workerStarted(AlignCalcWorkerAdapter source) + { + }; + + default void workerStopped(AlignCalcWorkerAdapter source) + { + }; + + default void workerHasResult(AlignCalcWorkerAdapter source, AnnotationResult result) + { + }; + + static final WorkerListener NULL_LISTENER = new WorkerListener() + { + }; + } + + private WorkerListener listener = WorkerListener.NULL_LISTENER; + + public void setWorkerListener(WorkerListener listener) + { + if (listener == null) listener = WorkerListener.NULL_LISTENER; + this.listener = listener; + } +} diff --git a/src/jalview/ws2/actions/annotation/AnnotationAction.java b/src/jalview/ws2/actions/annotation/AnnotationAction.java index 02829fd..b8450a9 100644 --- a/src/jalview/ws2/actions/annotation/AnnotationAction.java +++ b/src/jalview/ws2/actions/annotation/AnnotationAction.java @@ -3,6 +3,7 @@ package jalview.ws2.actions.annotation; import java.util.List; import java.util.Objects; +import jalview.api.AlignViewportI; import jalview.viewmodel.AlignmentViewport; import jalview.ws.params.ArgumentI; import jalview.ws2.actions.BaseAction; @@ -84,18 +85,14 @@ public class AnnotationAction extends BaseAction requireAlignedSequences = builder.requireAlignedSequences; filterSymbols = builder.filterSymbols; } - + @Override - public TaskI perform(AlignmentViewport viewport, - List args, Credentials credentials, - TaskEventListener handler) + public AnnotationTask createTask(AlignViewportI viewport, + List args, Credentials credentials) { - var task = new AnnotationTask(client, this, args, credentials, viewport, - handler); - task.start(viewport.getCalcManager()); - return task; + return new AnnotationTask(client, this, args, credentials, viewport); } - + /** * Return if this action is an alignment analysis service. */ diff --git a/src/jalview/ws2/actions/annotation/AnnotationProviderI.java b/src/jalview/ws2/actions/annotation/AnnotationProviderI.java index 3a836a0..d865352 100644 --- a/src/jalview/ws2/actions/annotation/AnnotationProviderI.java +++ b/src/jalview/ws2/actions/annotation/AnnotationProviderI.java @@ -32,7 +32,8 @@ public interface AnnotationProviderI * @param job * web service job * @param sequences - * sequences the annotations will be added to + * features and alignment annotation added to these will be + * imported to the dataset for the alignment * @param colours * container for feature colours * @param filters diff --git a/src/jalview/ws2/actions/annotation/AnnotationTask.java b/src/jalview/ws2/actions/annotation/AnnotationTask.java index 271d9ed..866f862 100644 --- a/src/jalview/ws2/actions/annotation/AnnotationTask.java +++ b/src/jalview/ws2/actions/annotation/AnnotationTask.java @@ -2,19 +2,13 @@ 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.bin.Console; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.AnnotatedCollectionI; @@ -23,306 +17,33 @@ 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.BaseTask; 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; -public class AnnotationTask implements TaskI +public class AnnotationTask extends BaseTask { - private final long uid = MathUtils.getUID(); - private AnnotationWebServiceClientI client; private final AnnotationAction action; - private final List args; - - private final Credentials credentials; - - private final AlignViewportI viewport; - - private final TaskEventSupport eventHandler; - - private JobStatus taskStatus = null; - - private AlignCalcWorkerAdapter worker = null; - - private List jobs = Collections.emptyList(); - - private AnnotationResult result = null; - - private DelegateJobEventListener 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 newAnnots) - { - List oldAnnots = ourAnnots != null ? 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(); - } + private final AlignmentI alignment; - @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()); - } - } + private final AnnotatedCollectionI selectionGroup; public AnnotationTask(AnnotationWebServiceClientI client, AnnotationAction action, List args, Credentials credentials, - AlignViewportI viewport, - TaskEventListener eventListener) + AlignViewportI viewport) { + super(client, args, credentials); 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 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) - { - Console.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 getSubJobs() - { - return jobs; + this.alignment = viewport.getAlignment(); + this.selectionGroup = viewport.getSelectionGroup(); } /** @@ -334,9 +55,9 @@ public class AnnotationTask implements TaskI * @throws ServiceInputInvalidException * input data is not valid */ - private List prepare() throws ServiceInputInvalidException + @Override + public List prepareJobs() throws ServiceInputInvalidException { - AlignmentI alignment = viewport.getAlignment(); if (alignment == null || alignment.getWidth() <= 0 || alignment.getSequences() == null) throw new ServiceInputInvalidException("Alignment does not contain sequences"); @@ -347,7 +68,7 @@ public class AnnotationTask implements TaskI throw new ServiceInputInvalidException( action.getFullName() + " does not allow protein sequences"); boolean bySequence = !action.isAlignmentAnalysis(); - AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null; + AnnotatedCollectionI inputSeqs = bySequence ? selectionGroup : null; if (inputSeqs == null || inputSeqs.getWidth() <= 0 || inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1) inputSeqs = alignment; @@ -366,38 +87,8 @@ public class AnnotationTask implements TaskI 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 + @Override + protected AnnotationResult collectResult(List jobs) throws IOException { final Map featureColours = new HashMap<>(); final Map featureFilters = new HashMap<>(); @@ -410,179 +101,88 @@ public class AnnotationTask implements TaskI * 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, Boolean> updateResultAnnotation( - AnnotationJob job, List annotations) - { - List 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) + udpateCalcId(returnedAnnot); + for (AlignmentAnnotation ala : returnedAnnot) { - 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()); + SequenceI seq = job.seqNames.get(ala.sequenceRef.getName()); + aseq = seq.getRootDatasetSequence(); } 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); + Annotation[] gappedAnnots = createGappedAnnotations(ala.annotations, job.start, job.gapMap); + ala.annotations = gappedAnnots; } - boolean transferFeatures = false; + boolean hasFeatures = false; for (SequenceI sq : job.getInputSequences()) { - if (!sq.getFeatures().hasFeatures() && - (sq.getDBRefs() == null || sq.getDBRefs().size() == 0)) + if (!sq.getFeatures().hasFeatures() && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty())) continue; - transferFeatures = true; + hasFeatures = 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 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(); - } + SequenceI datasetSeq = seq.getRootDatasetSequence(); + List sourceRange = findContiguousRanges(datasetSeq, job.gapMap, job.start, job.end); + int[] sourceStartEnd = ContiguousI.toStartEndArray(sourceRange); Mapping mp = new Mapping(new MapList( sourceStartEnd, new int[] - { seq.getStart(), seq.getEnd() }, 1, 1)); - dseq.transferAnnotation(sq, mp); + { datasetSeq.getStart(), datasetSeq.getEnd() }, 1, 1)); + datasetSeq.transferAnnotation(sq, mp); } - return new Pair<>(newAnnots, transferFeatures); - } - - @Override - public AnnotationResult getResult() - { - return result; + return new AnnotationResult(returnedAnnot, hasFeatures, featureColours, featureFilters); } - @Override - public void cancel() + /** + * Updates calcId on provided annotations if not already set. + */ + public void udpateCalcId(Iterable annotations) { - setStatus(JobStatus.CANCELLED); - if (worker != null) + for (var annotation : annotations) { - worker.stop(); + if (annotation.getCalcId() == null || annotation.getCalcId().isEmpty()) + { + annotation.setCalcId(action.getFullName()); + } + annotation.autoCalculated = action.isAlignmentAnalysis() && + action.getWebService().isInteractive(); } - cancelJobs(); } - public void cancelJobs() + private Annotation[] createGappedAnnotations(Annotation[] annotations, int start, boolean[] gapMap) { - for (BaseJob job : jobs) + var size = Math.max(alignment.getWidth(), gapMap.length); + Annotation[] gappedAnnotations = new Annotation[size]; + for (int p = 0, ap = start; ap < size; ap++) { - if (!job.isCompleted()) + if (gapMap != null && gapMap.length > ap && !gapMap[ap]) + { + gappedAnnotations[ap] = new Annotation("", "", ' ', Float.NaN); + } + else if (p < annotations.length) { - try - { - if (job.getServerJob() != null) - { - client.cancel(job.getServerJob()); - } - job.setStatus(JobStatus.CANCELLED); - } catch (IOException e) - { - Console.error(String.format( - "failed to cancel job %s", job.getServerJob()), e); - } + gappedAnnotations[ap] = annotations[p++]; } } + return gappedAnnotations; } - @Override - public String toString() + private List findContiguousRanges(SequenceI seq, boolean[] gapMap, int start, int end) { - var status = taskStatus != null ? taskStatus.name() : "UNSET"; - return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, status); + if (gapMap == null || gapMap.length < end) + return List.of(seq.findPositions(start, end)); + List ranges = new ArrayList<>(); + int lastcol = start, col = start; + do + { + if (col == end || !gapMap[col]) + { + if (lastcol < col) + ranges.add(seq.findPositions(lastcol, col)); + lastcol = col + 1; + } + } while (++col <= end); + return ranges; } } diff --git a/src/jalview/ws2/actions/api/ActionI.java b/src/jalview/ws2/actions/api/ActionI.java index 52d70df..e84fd58 100644 --- a/src/jalview/ws2/actions/api/ActionI.java +++ b/src/jalview/ws2/actions/api/ActionI.java @@ -5,6 +5,7 @@ import java.util.List; import javax.swing.Icon; +import jalview.api.AlignViewportI; import jalview.viewmodel.AlignmentViewport; import jalview.ws.params.ArgumentI; import jalview.ws2.api.CredentialType; @@ -115,10 +116,8 @@ public interface ActionI EnumSet 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. + * Create a new task with provided viewport, arguments and credentials ready + * to be started. * * @param viewport * current alignment viewport @@ -126,12 +125,10 @@ public interface ActionI * job parameters appropriate for the service * @param credentials * optional user credentials - * @param handler - * event handler attached to the new task - * @return new running task + * @return new task */ - TaskI perform(AlignmentViewport viewport, List args, - Credentials credentials, TaskEventListener handler); + TaskI createTask(AlignViewportI viewport, List args, + Credentials credentials); /** * Return if the action is currently active for the given viewport. Active diff --git a/src/jalview/ws2/actions/api/TaskEventListener.java b/src/jalview/ws2/actions/api/TaskEventListener.java index 94de9d0..b0bb4b6 100644 --- a/src/jalview/ws2/actions/api/TaskEventListener.java +++ b/src/jalview/ws2/actions/api/TaskEventListener.java @@ -2,6 +2,7 @@ package jalview.ws2.actions.api; import java.util.List; +import jalview.bin.Console; import jalview.ws2.api.JobStatus; import jalview.ws2.api.WebServiceJobHandle; @@ -29,7 +30,7 @@ public interface TaskEventListener * @param subJobs * list of sub-jobs for this run */ - void taskStarted(TaskI source, List subJobs); + default void taskStarted(TaskI source, List subJobs) {}; /** * Invoked when the global task status has changed. @@ -39,7 +40,7 @@ public interface TaskEventListener * @param status * new task status */ - void taskStatusChanged(TaskI source, JobStatus status); + default void taskStatusChanged(TaskI source, JobStatus status) {}; /** * Invoked when the task has completed. If the task completed with a result, @@ -51,7 +52,7 @@ public interface TaskEventListener * @param result * computation result or null if result not present */ - void taskCompleted(TaskI source, T result); + default void taskCompleted(TaskI source, T result) {}; /** * Invoked when an unhandled exception has occurred during task execution. @@ -61,17 +62,7 @@ public interface TaskEventListener * @param e * exception */ - void taskException(TaskI 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 source); + default void taskException(TaskI source, Exception e) {}; /** * Invoked when the status of a sub-job has changed. @@ -83,7 +74,7 @@ public interface TaskEventListener * @param status * new job status */ - void subJobStatusChanged(TaskI source, JobI job, JobStatus status); + default void subJobStatusChanged(TaskI source, JobI job, JobStatus status) {}; /** * Invoked when a log string of the sub-job has changed. @@ -95,7 +86,7 @@ public interface TaskEventListener * @param log * new log string */ - void subJobLogChanged(TaskI source, JobI job, String log); + default void subJobLogChanged(TaskI source, JobI job, String log) {}; /** * Invoked when an error log string of the sub-job has changed. @@ -107,5 +98,56 @@ public interface TaskEventListener * @param log * new log string */ - void subJobErrorLogChanged(TaskI source, JobI job, String log); + default void subJobErrorLogChanged(TaskI source, JobI job, String log) {}; + + @SuppressWarnings("rawtypes") + static final TaskEventListener NULL_LISTENER = new TaskEventListener() + { + @Override + public void taskStarted(TaskI source, List subJobs) + { + Console.info("task started with " + subJobs.size() + " jobs"); + } + + @Override + public void taskStatusChanged(TaskI source, JobStatus status) + { + Console.info("task status " + status); + } + + @Override + public void taskCompleted(TaskI source, Object result) + { + Console.info("task completed"); + } + + @Override + public void taskException(TaskI source, Exception e) + { + Console.info("task failed", e); + } + + @Override + public void subJobStatusChanged(TaskI source, JobI job, + JobStatus status) + { + Console.info("sub-job " + job.getInternalId() + " status " + status); + } + + @Override + public void subJobLogChanged(TaskI source, JobI job, String log) + { + } + + @Override + public void subJobErrorLogChanged(TaskI source, JobI job, String log) + { + } + }; + + @SuppressWarnings("unchecked") + static TaskEventListener nullListener() + { + return (TaskEventListener) NULL_LISTENER; + } } diff --git a/src/jalview/ws2/actions/api/TaskI.java b/src/jalview/ws2/actions/api/TaskI.java index cb84944..5f4d575 100644 --- a/src/jalview/ws2/actions/api/TaskI.java +++ b/src/jalview/ws2/actions/api/TaskI.java @@ -38,6 +38,10 @@ public interface TaskI */ List getSubJobs(); + void addTaskEventListener(TaskEventListener listener); + + void removeTaskEventListener(TaskEventListener listener); + /** * Get the last result of the task or {@code null} if not present. Note that * the result is subject to change for restartable tasks. @@ -46,6 +50,12 @@ public interface TaskI */ T getResult(); + public void init() throws Exception; + + public boolean poll() throws Exception; + + public void complete() throws Exception; + /** * Cancel the task, stop all sub-jobs running on a server and stop all threads * managing this task. diff --git a/src/jalview/ws2/actions/hmmer/PhmmerAction.java b/src/jalview/ws2/actions/hmmer/PhmmerAction.java new file mode 100644 index 0000000..bdba2f7 --- /dev/null +++ b/src/jalview/ws2/actions/hmmer/PhmmerAction.java @@ -0,0 +1,82 @@ +package jalview.ws2.actions.hmmer; + +import java.util.List; +import java.util.Objects; + +import jalview.api.AlignViewportI; +import jalview.datamodel.AlignmentI; +import jalview.viewmodel.AlignmentViewport; +import jalview.ws.params.ArgumentI; +import jalview.ws2.actions.BaseAction; +import jalview.ws2.actions.PollingTaskExecutor; +import jalview.ws2.actions.api.TaskEventListener; +import jalview.ws2.actions.api.TaskI; +import jalview.ws2.api.Credentials; +import jalview.ws2.client.api.AlignmentWebServiceClientI; +import jalview.ws2.client.api.WebServiceClientI; + +/** + * Implementation of the {@link BaseAction} for the phmmer client. This is NOT + * how you should implement it. The action should be more generic and cover + * range of similar services. + * + * @author mmwarowny + * + */ +// FIXME: Not an alignment action (temporary hack) +public class PhmmerAction extends BaseAction +{ + public static class Builder extends BaseAction.Builder + { + protected AlignmentWebServiceClientI client; + + private Builder(AlignmentWebServiceClientI client) + { + super(); + Objects.requireNonNull(client); + this.client = client; + } + + public PhmmerAction build() + { + return new PhmmerAction(this); + } + } + + public static Builder newBuilder(AlignmentWebServiceClientI client) + { + return new Builder(client); + } + + protected final AlignmentWebServiceClientI client; + + public PhmmerAction(Builder builder) + { + super(builder); + client = builder.client; + } + + public TaskI perform(AlignmentViewport viewport, + List args, Credentials credentials, + TaskEventListener handler) + { + var task = createTask(viewport, args, credentials); + task.addTaskEventListener(handler); + var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor()); + var future = executor.submit(task); + task.setCancelAction(() -> { future.cancel(true); }); + return task; + } + + public PhmmerTask createTask(AlignViewportI viewport, + List args, Credentials credentials) + { + return new PhmmerTask(client, args, credentials, viewport.getAlignmentView(true)); + } + + @Override + public boolean isActive(AlignmentViewport viewport) + { + return false; + } +} diff --git a/src/jalview/ws2/actions/hmmer/PhmmerTask.java b/src/jalview/ws2/actions/hmmer/PhmmerTask.java new file mode 100644 index 0000000..ede61d3 --- /dev/null +++ b/src/jalview/ws2/actions/hmmer/PhmmerTask.java @@ -0,0 +1,111 @@ +package jalview.ws2.actions.hmmer; + +import static jalview.util.Comparison.GapChars; + +import java.io.IOException; +import java.util.List; + +import jalview.analysis.AlignSeq; +import jalview.bin.Console; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.AlignmentView; +import jalview.datamodel.Annotation; +import jalview.datamodel.Sequence; +import jalview.datamodel.SequenceI; +import jalview.util.Comparison; +import jalview.ws.params.ArgumentI; +import jalview.ws2.actions.BaseJob; +import jalview.ws2.actions.BaseTask; +import jalview.ws2.actions.ServiceInputInvalidException; +import jalview.ws2.api.Credentials; +import jalview.ws2.api.JobStatus; +import jalview.ws2.client.api.AlignmentWebServiceClientI; + +class PhmmerTask extends BaseTask +{ + private final AlignmentWebServiceClientI client; + private final AlignmentView view; + + PhmmerTask(AlignmentWebServiceClientI client, List args, + Credentials credentials, AlignmentView view) + { + super(client, args, credentials); + this.client = client; + this.view = view; + } + + @Override + protected List prepareJobs() throws ServiceInputInvalidException + { + Console.info("Preparing sequence for phmmer job"); + var sequence = view.getVisibleAlignment('-').getSequenceAt(0); + var seq = new Sequence(sequence.getName(), + AlignSeq.extractGaps(GapChars, sequence.getSequenceAsString())); + var job = new BaseJob(List.of(seq)) + { + @Override + public boolean isInputValid() + { + return true; + } + }; + job.setStatus(JobStatus.READY); + return List.of(job); + } + + @Override + protected AlignmentI collectResult(List jobs) throws IOException + { + var job = jobs.get(0); + var status = job.getStatus(); + Console.info(String.format("phmmer finished job \"%s\" with status %s", + job.getServerJob().getJobId(), status)); + if (status != JobStatus.COMPLETED) + return null; + var outputAlignment = client.getAlignment(job.getServerJob()); + var querySeq = job.getInputSequences().get(0).deriveSequence(); + { + AlignmentAnnotation refpos = null; + for (var annot : outputAlignment.getAlignmentAnnotation()) + { + if (annot.sequenceRef == null && annot.label.equals("Reference Positions")) + { + refpos = annot; + break; + } + } + if (refpos != null) + { + querySeq = alignQeuryToReferencePositions(querySeq, refpos); + } + } + outputAlignment.insertSequenceAt(0, querySeq); + return outputAlignment; + } + + private SequenceI alignQeuryToReferencePositions(SequenceI query, AlignmentAnnotation refpos) + { + var sequenceBuilder = new StringBuilder(); + var index = 0; + for (Annotation a : refpos.annotations) + { + // TODO: we assume that the number of "x" annotations is equal to the number + // of residues. may need a safeguard against invalid input + if (a != null && a.displayCharacter.equals("x")) + { + char c; + do + c = query.getCharAt(index++); + while (Comparison.isGap(c)); + sequenceBuilder.append(c); + } + else + { + sequenceBuilder.append(Comparison.GAP_DASH); + } + } + query.setSequence(sequenceBuilder.toString()); + return query; + } +} diff --git a/src/jalview/ws2/api/Credentials.java b/src/jalview/ws2/api/Credentials.java index cc7c714..5e482ab 100644 --- a/src/jalview/ws2/api/Credentials.java +++ b/src/jalview/ws2/api/Credentials.java @@ -5,19 +5,39 @@ 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() { + private Credentials() + { + } + + public String getUsername() + { + return username; + } + + public String getEmail() + { + return email; } - + + public String getPassword() + { + return password; + } + public static final Credentials empty() { return EMPTY; } - public static final Credentials usingEmail(String email) { + public static final Credentials usingEmail(String email) + { Objects.requireNonNull(email); if (email.isEmpty()) throw new IllegalArgumentException("empty email"); @@ -25,8 +45,9 @@ public final class Credentials credentials.email = email; return credentials; } - - public static final Credentials usingEmail(String email, String password) { + + public static final Credentials usingEmail(String email, String password) + { Objects.requireNonNull(email); Objects.requireNonNull(password); if (email.isEmpty()) @@ -36,8 +57,10 @@ public final class Credentials credentials.password = password; return credentials; } - - public static final Credentials usingUsername(String username, String password) { + + public static final Credentials usingUsername(String username, + String password) + { Objects.requireNonNull(username); Objects.requireNonNull(password); if (username.isEmpty()) diff --git a/src/jalview/ws2/client/ebi/JobDispatcherWSDiscoverer.java b/src/jalview/ws2/client/ebi/JobDispatcherWSDiscoverer.java new file mode 100644 index 0000000..48d0ba5 --- /dev/null +++ b/src/jalview/ws2/client/ebi/JobDispatcherWSDiscoverer.java @@ -0,0 +1,107 @@ +package jalview.ws2.client.ebi; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; + +import jalview.bin.Console; +import jalview.ws.params.ParamManager; +import jalview.ws2.actions.NullAction; +import jalview.ws2.actions.api.ActionI; +import jalview.ws2.actions.hmmer.PhmmerAction; +import jalview.ws2.api.WebService; +import jalview.ws2.client.api.AbstractWebServiceDiscoverer; +import uk.ac.dundee.compbio.hmmerclient.PhmmerClient; + +public final class JobDispatcherWSDiscoverer extends AbstractWebServiceDiscoverer +{ + + private static final URL DEFAULT_URL; + static + { + try + { + DEFAULT_URL = new URL("https://www.ebi.ac.uk/Tools/services/rest/hmmer3_phmmer/"); + } catch (MalformedURLException e) + { + throw new ExceptionInInitializerError(e); + } + } + + private static JobDispatcherWSDiscoverer instance = null; + private static ParamManager paramManager = null; + + private JobDispatcherWSDiscoverer() {} + + public static JobDispatcherWSDiscoverer getInstance() { + if (instance == null) + instance = new JobDispatcherWSDiscoverer(); + return instance; + } + + public static void setParamManager(ParamManager manager) + { + paramManager = manager; + } + + @Override + public int getStatusForUrl(URL url) + { + try + { + return new PhmmerClient(url).testEndpoint() ? STATUS_OK : STATUS_INVALID; + } catch (URISyntaxException e) + { + Console.error(e.getMessage()); + return STATUS_INVALID; + } + } + + @Override + protected String getUrlsPropertyKey() + { + return null; + } + + @Override + protected URL getDefaultUrl() + { + return DEFAULT_URL; + } + + @Override + protected List> fetchServices(URL url) throws IOException + { + PhmmerClient phmmerClient; + try { + phmmerClient = new PhmmerClient(url); + } + catch (URISyntaxException e) { + throw new MalformedURLException(e.getMessage()); + } + if (!phmmerClient.testEndpoint()) + throw new IOException( + "unable to reach dispatcher server at " + url); + var wsBuilder = WebService. newBuilder(); + wsBuilder.url(url); + wsBuilder.clientName("job dispatcher"); + wsBuilder.category("Database search"); + wsBuilder.name("pHMMER"); + wsBuilder.description("Hmmer3 phmmer is used to search one or more query sequences against a sequence database."); + wsBuilder.interactive(false); + wsBuilder.paramDatastore(ParamStores.newPhmmerDatastore(url, paramManager)); + wsBuilder.actionClass(PhmmerAction.class); + var webService = wsBuilder.build(); + + var client = new PhmmerWSClient(phmmerClient); + var actionBuilder = PhmmerAction.newBuilder(client); + actionBuilder.webService(webService); + actionBuilder.name(""); + webService.addAction(actionBuilder.build()); + + return List.of(webService); + } + +} diff --git a/src/jalview/ws2/client/ebi/ParamStores.java b/src/jalview/ws2/client/ebi/ParamStores.java new file mode 100644 index 0000000..ffd338d --- /dev/null +++ b/src/jalview/ws2/client/ebi/ParamStores.java @@ -0,0 +1,285 @@ +package jalview.ws2.client.ebi; + +import jalview.ws.params.ArgumentI; +import jalview.ws.params.ParamDatastoreI; +import jalview.ws.params.ParamManager; +import jalview.ws.params.simple.*; +import jalview.ws2.params.SimpleParamDatastore; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +class ParamStores +{ + static final List phmmerParameters; + + /** + * Set up phmmer parameters list. + */ + static + { + var parameters = new ArrayList(); + URL baseURL; + try + { + baseURL = new URL( "https://www.ebi.ac.uk/Tools/services/rest/hmmer3_phmmer/"); + } catch (MalformedURLException e) + { + throw new ExceptionInInitializerError(e); + } + { + var builder = StringParameter.newBuilder(); + builder.setName("cut-offs"); + builder.setLabel("Cut-offs"); + builder.setDescription("Set the method of controlling which target sequences match your query."); + builder.setRequired(true); + builder.setValue("E"); + builder.setDefaultValue("E"); + builder.setPossibleValues(List.of("E", "T")); + builder.setDisplayValues(List.of("E-values", "Bit scores")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("incE"); + builder.setLabel("Significance E-values[Sequence]"); + builder.setDescription("Significance E-values[Sequence]"); + builder.setRequired(false); + builder.setValue(0.01); + builder.setDefaultValue(0.01); + builder.setMin(0.); + builder.setMax(10.); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/incE")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("incdomE"); + builder.setLabel("Significance E-values[Hit]"); + builder.setDescription("Significance E-values[Hit]"); + builder.setRequired(false); + builder.setValue(0.03); + builder.setDefaultValue(0.03); + builder.setMin(0.); + builder.setMax(10.); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/incdomE")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("E"); + builder.setLabel("Report E-values[Sequence]"); + builder.setDescription("Report E-values[Sequence]"); + builder.setRequired(false); + builder.setValue(1.0); + builder.setDefaultValue(1.0); + builder.setMin(0.); + builder.setMax(10.); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/E")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("domE"); + builder.setLabel("Report E-values[Hit]"); + builder.setDescription("Report E-values[Hit]"); + builder.setRequired(false); + builder.setValue(1.0); + builder.setDefaultValue(1.0); + builder.setMin(0.); + builder.setMax(10.); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/domE")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("incT"); + builder.setLabel("Significance bit scores[Sequence]"); + builder.setDescription("Significance bit scores[Sequence]"); + builder.setRequired(false); + builder.setValue(25.0); + builder.setDefaultValue(25.0); + builder.setMin(0.); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/incT")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("incdomT"); + builder.setLabel("Significance bit scores[Hit]"); + builder.setDescription("Significance bit scores[Hit]"); + builder.setRequired(false); + builder.setValue(22.0); + builder.setDefaultValue(22.0); + builder.setMin(0.); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/incdomT")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("T"); + builder.setLabel("Report bit scores[Sequence]"); + builder.setDescription("Report bit scores[Sequence]"); + builder.setRequired(false); + builder.setValue(7.0); + builder.setDefaultValue(7.0); + builder.setMin(0.); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/T")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("domT"); + builder.setLabel("Report bit scores[Hit]"); + builder.setDescription("Report bit scores[Hit]"); + builder.setRequired(false); + builder.setValue(5.0); + builder.setDefaultValue(5.0); + builder.setMin(0.); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/domT")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("popen"); + builder.setLabel("Gap Penalties[open]"); + builder.setDescription("Gap Penalties[open]"); + builder.setRequired(false); + builder.setValue(0.02); + builder.setDefaultValue(0.02); + builder.setMin(0.); + builder.setMax(0.5); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/popen")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("pextend"); + builder.setLabel("Gap Penalties[extend]"); + builder.setDescription("Gap Penalties[extend]"); + builder.setRequired(false); + builder.setValue(0.4); + builder.setDefaultValue(0.4); + builder.setMin(0.); + builder.setMax(1.); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/pextend")); + parameters.add(builder.build()); + } + { + var builder = StringParameter.newBuilder(); + builder.setName("mx"); + builder.setLabel("Gap Penalties[Substitution scoring matrix]"); + builder.setDescription("Gap Penalties[Substitution scoring matrix]"); + builder.setRequired(false); + builder.setValue("BLOSUM62"); + builder.setDefaultValue("BLOSUM62"); + builder.setPossibleValues(List.of("BLOSUM45", "BLOSUM62", "BLOSUM90", "PAM30", "PAM70")); + builder.setDisplayValues(List.of("BLOSUM45", "BLOSUM62", "BLOSUM90", "PAM30", "PAM70")); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/mx")); + parameters.add(builder.build()); + } + { + var builder = BooleanOption.newBuilder(); + builder.setName("nobias"); + builder.setLabel("No bias filter"); + builder.setDescription( + "The '--nobias' option turns off (bypasses) the biased composition filter which is on by default."); + builder.setRequired(false); + builder.setValue(false); + builder.setDefaultValue(false); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/nobias")); + parameters.add(builder.build()); + } + { + var builder = BooleanOption.newBuilder(); + builder.setName("compressedout"); + builder.setLabel("Compressed Output"); + builder.setDescription( + "By default it runs hmm2c plus post-processing (default output), whereas with compressedout, it gets compressed output only."); + builder.setRequired(false); + builder.setValue(false); + builder.setDefaultValue(false); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/compressedout")); + parameters.add(builder.build()); + } + { + var builder = BooleanOption.newBuilder(); + builder.setName("alignView"); + builder.setLabel("Output Alignment"); + builder.setDescription("Output alignment in result"); + builder.setRequired(false); + builder.setValue(true); + builder.setDefaultValue(true); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/alignView")); + parameters.add(builder.build()); + } + { + var builder = StringParameter.newBuilder(); + builder.setName("database"); + builder.setLabel("Sequence Database"); + builder.setDescription("Sequence Database Selection"); + builder.setRequired(true); + builder.setValue("uniprotkb"); + builder.setDefaultValue("uniprotkb"); + builder.setPossibleValues(List.of("swissprot", "uniprotrefprot", "uniprotkb", "pdb", "rp75", "rp55", "rp35", "rp15", "ensembl", "merops", "qfo", "chembl")); + builder.setDisplayValues(List.of("SwissProt", "Reference Proteomes", "UniProtKB", "PDB", "rp75", "rp55", "rp35", "rp15", "Ensembl", "MEROPS", "Quest for Orthologs", "ChEMBL")); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/database")); + parameters.add(builder.build()); + } + { + var builder = DoubleParameter.newBuilder(); + builder.setName("evalue"); + builder.setLabel("Expectation Value Threshold"); + builder.setDescription( + "Expectation value cut-off for reporting target profiles in the per-target output."); + builder.setRequired(false); + builder.setValue(0.01); + builder.setDefaultValue(0.01); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/evalue")); + parameters.add(builder.build()); + } +// { +// var builder = StringParameter.newBuilder(); +// builder.setName("sequence"); +// builder.setLabel("Input Sequence"); +// builder.setDescription( +// "The input sequence can be entered directly into this form. The sequence can be be in FASTA or UniProtKB/Swiss-Prot format. A partially formatted sequence is not accepted. Adding a return to the end of the sequence may help certain applications understand the input. Note that directly using data from word processors may yield unpredictable results as hidden/control characters may be present."); +// builder.setRequired(false); +// +// builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/sequence")); +// parameters.add(builder.build()); +// } + { + var builder = IntegerParameter.newBuilder(); + builder.setName("nhits"); + builder.setLabel("Number of Hits Displayed"); + builder.setDescription("Number of hits to be displayed."); + builder.setRequired(false); + builder.setValue(100); + builder.setDefaultValue(100); + builder.setDetailsUrl(resolveURL(baseURL, "parameterdetails/nhits")); + parameters.add(builder.build()); + } + phmmerParameters = Collections.unmodifiableList(parameters); + } + + private static URL resolveURL(URL base, String spec) + { + try + { + return new URL(base, spec); + } catch (MalformedURLException e) + { + throw new ExceptionInInitializerError(e); + } + } + + public static ParamDatastoreI newPhmmerDatastore(URL url, ParamManager manager) + { + return new SimpleParamDatastore(url, phmmerParameters, + Collections.emptyList(), manager); + } +} diff --git a/src/jalview/ws2/client/ebi/PhmmerWSClient.java b/src/jalview/ws2/client/ebi/PhmmerWSClient.java new file mode 100644 index 0000000..380e8e6 --- /dev/null +++ b/src/jalview/ws2/client/ebi/PhmmerWSClient.java @@ -0,0 +1,271 @@ +package jalview.ws2.client.ebi; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.net.URI; +import java.util.List; + +import jalview.bin.Console; +import jalview.datamodel.Alignment; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.SequenceI; +import jalview.io.DataSourceType; +import jalview.io.FileFormat; +import jalview.io.FileParse; +import jalview.io.FormatAdapter; +import jalview.io.StockholmFile; +import jalview.ws.params.ArgumentI; +import jalview.ws.params.simple.BooleanOption; +import jalview.ws.params.simple.DoubleParameter; +import jalview.ws.params.simple.IntegerParameter; +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.WebServiceClientI; +import uk.ac.dundee.compbio.hmmerclient.PhmmerClient; +import uk.ac.dundee.compbio.hmmerclient.PhmmerRequest; +import uk.ac.dundee.compbio.hmmerclient.PhmmerRequest.SequenceDatabase; +import uk.ac.dundee.compbio.hmmerclient.PhmmerRequest.SubstitutionMatrix; + +public class PhmmerWSClient implements AlignmentWebServiceClientI +{ + + final PhmmerClient client; + + PhmmerWSClient(PhmmerClient client) + { + this.client = client; + } + + @Override + public String getUrl() + { + return client.getURL().toString(); + } + + @Override + public String getClientName() + { + return "ebi-job-dispatcher"; + } + + @Override + public WebServiceJobHandle submit(List sequences, + List args, Credentials credentials) throws IOException + { + var request = PhmmerRequest.newBuilder(); + String sequence = FileFormat.Fasta.getWriter(null) + .print(new SequenceI[]{ sequences.get(0) }, false); + request.sequence(new StringReader(sequence)); + populateRequestArguments(request, args); + var email = credentials.getEmail() != null ? credentials.getEmail() : + "nouser@jalview.org"; + var jobId = client.submit(request.build(), email); + Console.debug("Phmmer client submitted new job with id " + jobId); + return new WebServiceJobHandle( + getClientName(), "phmmer", getUrl(), jobId); + } + + private static void populateRequestArguments(PhmmerRequest.Builder request, List args) + { + boolean useBitScore = false; + boolean useEValue = false; + for (var arg : args) + { + if (arg.getName().equals("cut-offs")) + if (arg.getValue().equals("E")) + useEValue = true; + else if (arg.getValue().equals("T")) + useBitScore = true; + else + throw new IllegalArgumentException( + "cut-offs argument contains value other than \"E\" or \"T\": " + + arg.getValue()); + } + assert (useBitScore || useEValue) && !(useBitScore && useEValue); + for (var arg : args) + { + switch (arg.getName()) + { + case "incE": + request.incE(useEValue ? DoubleParameter.parseFloat(arg) : null); + break; + case "incdomE": + request.incdomE(useEValue ? DoubleParameter.parseFloat(arg) : null); + break; + case "E": + request.E(useEValue ? DoubleParameter.parseFloat(arg) : null); + break; + case "domE": + request.domE(useEValue ? DoubleParameter.parseFloat(arg) : null); + break; + case "incT": + request.incT(useBitScore ? DoubleParameter.parseFloat(arg) : null); + break; + case "incdomT": + request.incdomT(useBitScore ? DoubleParameter.parseFloat(arg) : null); + break; + case "T": + request.T(useBitScore ? DoubleParameter.parseFloat(arg) : null); + break; + case "domT": + request.domT(useBitScore ? DoubleParameter.parseFloat(arg) : null); + break; + case "popen": + request.popen(DoubleParameter.parseFloat(arg)); + break; + case "pextend": + request.pextend(DoubleParameter.parseFloat(arg)); + break; + case "mx": + request.mx(parseSubstitutionMatrix(arg)); + break; + case "nobias": + request.noBias(BooleanOption.parseBoolean(arg)); + break; + case "compressedout": + request.compressedOut(BooleanOption.parseBoolean(arg)); + break; + case "alignView": + request.compressedOut(BooleanOption.parseBoolean(arg)); + break; + case "database": + request.database(parseSequenceDatabase(arg)); + break; + case "evalue": + request.evalue(DoubleParameter.parseFloat(arg)); + break; + case "nhits": + request.nhits(IntegerParameter.parseInt(arg)); + break; + } + } + } + + private static SubstitutionMatrix parseSubstitutionMatrix(ArgumentI arg) + { + if (arg.getValue() == null) + return null; + switch (arg.getValue()) + { + case "BLOSUM45": + return SubstitutionMatrix.BLOSUM45; + case "BLOSUM62": + return SubstitutionMatrix.BLOSUM62; + case "BLOSUM90": + return SubstitutionMatrix.BLOSUM90; + case "PAM30": + return SubstitutionMatrix.PAM30; + case "PAM70": + return SubstitutionMatrix.PAM70; + default: + throw new IllegalArgumentException( + "invalid matrix " + arg.getValue()); + } + } + + private static SequenceDatabase parseSequenceDatabase(ArgumentI arg) + { + if (arg.getValue() == null) + return null; + switch (arg.getValue()) + { + case "swissprot": + return SequenceDatabase.SWISS_PROT; + case "uniprotrefprot": + return SequenceDatabase.REFERENCE_PROTEOMES; + case "uniprotkb": + return SequenceDatabase.UNIPROTKB; + case "pdb": + return SequenceDatabase.PDB; + case "rp75": + return SequenceDatabase.RP75; + case "rp55": + return SequenceDatabase.RP55; + case "rp35": + return SequenceDatabase.RP35; + case "rp15": + return SequenceDatabase.RP15; + case "ensembl": + return SequenceDatabase.ENSEMBL; + case "merops": + return SequenceDatabase.MEROPS; + case "qfo": + return SequenceDatabase.QUEST_FOR_ORTHOLOGS; + case "chembl": + return SequenceDatabase.CHEMBL; + default: + throw new IllegalArgumentException( + "invalid database " + arg.getValue()); + } + } + + @Override + public JobStatus getStatus(WebServiceJobHandle job) throws IOException + { + var status = client.getStatus(job.getJobId()); + switch (status) + { + case PENDING: return JobStatus.SUBMITTED; + case QUEUED: return JobStatus.QUEUED; + case RUNNING: return JobStatus.RUNNING; + case FINISHED: return JobStatus.COMPLETED; + case FAILURE: return JobStatus.FAILED; + case ERROR: return JobStatus.SERVER_ERROR; + case NOT_FOUND: return JobStatus.SERVER_ERROR; + case UNDEFINED: return JobStatus.UNKNOWN; + } + return JobStatus.UNKNOWN; + } + + @Override + public String getLog(WebServiceJobHandle job) throws IOException + { + return ""; + } + + @Override + public String getErrorLog(WebServiceJobHandle job) throws IOException + { + if (getStatus(job) != JobStatus.FAILED) + return ""; + try(InputStream stream = client.getResultStream(job.getJobId(), "error")) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + stream.transferTo(out); + return out.toString(); + } + } + + @Override + public void cancel(WebServiceJobHandle job) + throws IOException, UnsupportedOperationException + { + throw new UnsupportedOperationException( + "ebi job dispatcher does not support job cancellation"); + } + + /** + * FIXME: Temporary hack + */ + @Override + public AlignmentI getAlignment(WebServiceJobHandle job) throws IOException + { + URI url = client.getResultURL(job.getJobId(), "sto"); + try(InputStream stream = client.getResultStream(job.getJobId(), "sto")) + { + StockholmFile file = new StockholmFile(new FileParse( + new BufferedReader(new InputStreamReader(stream)), + url.toString(), DataSourceType.URL)); + var aln = new Alignment(file.getSeqsAsArray()); + for (var annotation : file.getAnnotations()) + aln.addAnnotation(annotation); + return aln; + } + } +} diff --git a/src/jalview/ws2/client/slivka/SlivkaWSClient.java b/src/jalview/ws2/client/slivka/SlivkaWSClient.java index 7dcdae1..477bc34 100644 --- a/src/jalview/ws2/client/slivka/SlivkaWSClient.java +++ b/src/jalview/ws2/client/slivka/SlivkaWSClient.java @@ -272,7 +272,8 @@ class SlivkaAnnotationWSClient extends SlivkaWSClient { FeaturesFile ff = new FeaturesFile(f.getContentUrl().toString(), DataSourceType.URL); - featPresent = ff.parse(aln, colours, true); + // TODO: determine if relaxed id matching is T/F + featPresent = ff.parse(aln, colours, filters, false, true); if (featPresent) Console.debug(format("loaded features for %s", service.getId())); } diff --git a/src/jalview/ws2/gui/AlignmentServiceGuiHandler.java b/src/jalview/ws2/gui/AlignmentServiceGuiHandler.java index fd1d0ad..caa0573 100644 --- a/src/jalview/ws2/gui/AlignmentServiceGuiHandler.java +++ b/src/jalview/ws2/gui/AlignmentServiceGuiHandler.java @@ -230,12 +230,6 @@ class AlignmentServiceGuiHandler } @Override - public void taskRestarted(TaskI source) - { - // alignment services are not restartable - } - - @Override public void subJobStatusChanged(TaskI source, JobI job, JobStatus status) { int i = ArrayUtils.indexOf(jobs, job); diff --git a/src/jalview/ws2/gui/AnnotationServiceGuiHandler.java b/src/jalview/ws2/gui/AnnotationServiceGuiHandler.java index 43e2680..a7202e9 100644 --- a/src/jalview/ws2/gui/AnnotationServiceGuiHandler.java +++ b/src/jalview/ws2/gui/AnnotationServiceGuiHandler.java @@ -1,21 +1,22 @@ package jalview.ws2.gui; -import java.util.List; - +import jalview.api.FeatureColourI; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.gui.AlignFrame; import jalview.gui.AlignmentPanel; import jalview.gui.IProgressIndicator; import jalview.gui.IProgressIndicatorHandler; +import jalview.schemes.FeatureSettingsAdapter; +import jalview.util.MathUtils; +import jalview.ws2.actions.annotation.AlignCalcWorkerAdapter; 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 + implements AlignCalcWorkerAdapter.WorkerListener { + private final long progressId = MathUtils.getUID(); + private final AlignFrame alignFrame; private final AlignmentPanel alignPanel; @@ -33,9 +34,10 @@ public class AnnotationServiceGuiHandler } @Override - public void taskStarted(TaskI source, List subJobs) + public void workerStarted(AlignCalcWorkerAdapter source) { - progressIndicator.registerHandler(source.getUid(), + progressIndicator.addProgressBar(progressId, action.getFullName()); + progressIndicator.registerHandler(progressId, new IProgressIndicatorHandler() { @Override @@ -54,67 +56,44 @@ public class AnnotationServiceGuiHandler } @Override - public void taskStatusChanged(TaskI source, JobStatus status) + public void workerStopped(AlignCalcWorkerAdapter source) { - 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; - } + progressIndicator.removeProgressBar(progressId); } @Override - public void taskCompleted(TaskI source, AnnotationResult result) + public void workerHasResult(AlignCalcWorkerAdapter source, final AnnotationResult result) { if (result == null) return; - if (result.getTransferFeatures() && alignFrame.alignPanel == alignPanel) + if (result.getTransferFeatures()) { - alignFrame.getViewport().setShowSequenceFeatures(true); - alignFrame.setMenusForViewport(); - } - alignPanel.adjustAnnotationHeight(); - } - - @Override - public void taskException(TaskI source, Exception e) - { - - } - - @Override - public void taskRestarted(TaskI source) - { - - } - - @Override - public void subJobStatusChanged(TaskI source, JobI job, JobStatus status) - { - - } - - @Override - public void subJobLogChanged(TaskI source, JobI job, String log) - { - - } + alignFrame.getViewport().applyFeaturesStyle(new FeatureSettingsAdapter() + { + @Override + public FeatureColourI getFeatureColour(String type) + { + return result.getFeatureColours().get(type); + } - @Override - public void subJobErrorLogChanged(TaskI source, JobI job, String log) - { + @Override + public FeatureMatcherSetI getFeatureFilters(String type) + { + return result.getFeatureFilters().get(type); + } + @Override + public boolean isFeatureDisplayed(String type) + { + return result.getFeatureColours().containsKey(type); + } + }); + if (alignFrame.alignPanel == alignPanel) + { + alignFrame.getViewport().setShowSequenceFeatures(true); + alignFrame.setMenusForViewport(); + } + } + alignPanel.adjustAnnotationHeight(); } } diff --git a/src/jalview/ws2/gui/SearchServiceGuiHandler.java b/src/jalview/ws2/gui/SearchServiceGuiHandler.java new file mode 100644 index 0000000..49df466 --- /dev/null +++ b/src/jalview/ws2/gui/SearchServiceGuiHandler.java @@ -0,0 +1,236 @@ +package jalview.ws2.gui; + +import java.util.List; + +import javax.swing.SwingUtilities; + +import jalview.bin.Console; +import jalview.datamodel.Alignment; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.AlignmentI; +import jalview.gui.AlignFrame; +import jalview.gui.Desktop; +import jalview.gui.JvOptionPane; +import jalview.gui.WebserviceInfo; +import jalview.util.ArrayUtils; +import jalview.util.MessageManager; +import jalview.ws2.actions.api.ActionI; +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; + +import static java.lang.String.format; + +class SearchServiceGuiHandler implements TaskEventListener +{ + private final AlignFrame parentFrame; + + private final ActionI action; + + private final WebService service; + + private WebserviceInfo infoPanel; + + 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 SearchServiceGuiHandler(ActionI action, AlignFrame parentFrame) + { + this.parentFrame = parentFrame; + this.action = action; + this.service = action.getWebService(); + var info = String.format("%s search using service at %s%n%s", + service.getName(), service.getUrl(), service.getDescription()); + this.infoPanel = new WebserviceInfo(service.getName(), info, false); + } + + @Override + public void taskStarted(TaskI source, + List subJobs) + { + Console.debug(format("task %s#%x started with %d sub-jobs", + service.getName(), source.getUid(), subJobs.size())); + jobs = subJobs.toArray(new JobI[subJobs.size()]); + 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(format("region %d", i), tabIndex); + infoPanel.setProgressText(tabIndex, "Job 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 taskStatusChanged(TaskI source, JobStatus status) + { + Console.debug(format("task %s#%x status changed to %s", + service.getName(), source.getUid(), status)); + switch (status) + { + case INVALID: + infoPanel.setVisible(false); + JvOptionPane.showMessageDialog(parentFrame, + MessageManager.getString("info.invalid_search_input"), + MessageManager.getString("info.invalid_search_input"), + 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 taskCompleted(TaskI source, AlignmentI result) + { + Console.debug(format("task %s#%x completed", service.getName(), + source.getUid())); + SwingUtilities.invokeLater( + () -> infoPanel.removeProgressBar(jobs[0].getInternalId())); + if (result == null) + { + SwingUtilities.invokeLater(infoPanel::setFinishedNoResults); + return; + } + infoPanel.showResultsNewFrame.addActionListener(evt -> { + // copy alignment for each frame to have its own instance + var alnCpy = new Alignment(result); + alnCpy.setGapCharacter(result.getGapCharacter()); + alnCpy.setDataset(result.getDataset()); + for (AlignmentAnnotation annotation : result.getAlignmentAnnotation()) + alnCpy.addAnnotation(new AlignmentAnnotation(annotation)); + displayResultsNewFrame(alnCpy); + }); + SwingUtilities.invokeLater(infoPanel::setResultsReady); + } + + private void displayResultsNewFrame(AlignmentI aln) + { + AlignFrame frame = new AlignFrame(aln, AlignFrame.DEFAULT_WIDTH, + AlignFrame.DEFAULT_HEIGHT); + frame.getFeatureRenderer().transferSettings( + parentFrame.getFeatureRenderer().getSettings()); + var actionName = action.getName() != null ? action.getName() : "Search"; + var title = String.format("%s %s of %s", service.getName(), actionName, + parentFrame.getTitle()); + Desktop.addInternalFrame(frame, title, AlignFrame.DEFAULT_WIDTH, + AlignFrame.DEFAULT_HEIGHT); + } + + @Override + public void taskException(TaskI source, Exception e) + { + Console.error(format("Task %s#%x raised an exception.", + service.getName(), source.getUid()), e); + infoPanel.appendProgressText(e.getMessage()); + } + + @Override + public void subJobStatusChanged(TaskI source, JobI job, + JobStatus status) + { + Console.debug(format("sub-job %x status changed to %s", + job.getInternalId(), 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 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 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])); + } +} diff --git a/src/jalview/ws2/gui/WebServicesMenuManager.java b/src/jalview/ws2/gui/WebServicesMenuManager.java index d6cf5b7..3757199 100644 --- a/src/jalview/ws2/gui/WebServicesMenuManager.java +++ b/src/jalview/ws2/gui/WebServicesMenuManager.java @@ -3,17 +3,14 @@ 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; @@ -24,6 +21,8 @@ import javax.swing.JMenuItem; import javax.swing.ToolTipManager; import javax.swing.border.EmptyBorder; +import jalview.bin.Console; +import jalview.datamodel.AlignmentI; import jalview.gui.AlignFrame; import jalview.gui.Desktop; import jalview.gui.JvSwingUtils; @@ -33,16 +32,20 @@ import jalview.viewmodel.AlignmentViewport; import jalview.ws.params.ArgumentI; import jalview.ws.params.ParamDatastoreI; import jalview.ws.params.WsParamSetI; +import jalview.ws2.actions.BaseTask; +import jalview.ws2.actions.PollingTaskExecutor; import jalview.ws2.actions.alignment.AlignmentAction; +import jalview.ws2.actions.alignment.AlignmentResult; +import jalview.ws2.actions.annotation.AlignCalcWorkerAdapter; import jalview.ws2.actions.annotation.AnnotationAction; import jalview.ws2.actions.api.ActionI; +import jalview.ws2.actions.api.TaskEventListener; import jalview.ws2.actions.api.TaskI; +import jalview.ws2.actions.hmmer.PhmmerAction; import jalview.ws2.api.Credentials; import jalview.ws2.api.WebService; import jalview.ws2.client.api.WebServiceProviderI; -import static java.lang.String.format; - public class WebServicesMenuManager { private final JMenu menu; @@ -58,8 +61,6 @@ public class WebServicesMenuManager noServicesItem.setEnabled(false); } - private Map>> interactiveTasks = new HashMap<>(); - public WebServicesMenuManager(String name, AlignFrame frame) { this.frame = frame; @@ -164,7 +165,8 @@ public class WebServicesMenuManager // sort actions by name pulling nulls to the front actions.sort(Comparator.comparing( ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder()))); - for (int i = 0; i < actions.size(); i++) { + for (int i = 0; i < actions.size(); i++) + { addEntriesForAction(actions.get(i), atMenu, atMenu == menu); } } @@ -367,15 +369,8 @@ public class WebServicesMenuManager serviceItem.removeActionListener(l); WebService service = action.getWebService(); serviceItem.addActionListener(e -> { - if (serviceItem.getState()) - { - cancelAndRunInteractive(action, frame.getCurrentView(), arguments, - Credentials.empty()); - } - else - { - cancelInteractive(service.getName()); - } + runAction(action, frame.getCurrentView(), arguments, + Credentials.empty()); }); serviceItem.setSelected(true); @@ -394,7 +389,7 @@ public class WebServicesMenuManager lastPreset[0] = null; arguments.clear(); arguments.addAll(args); - cancelAndRunInteractive(action, frame.getCurrentView(), + runAction(action, frame.getCurrentView(), arguments, Credentials.empty()); } }); @@ -411,14 +406,14 @@ public class WebServicesMenuManager var item = new JMenuItem(preset.getName()); item.addActionListener(e -> { lastPreset[0] = preset; - cancelAndRunInteractive(action, frame.getCurrentView(), + runAction(action, frame.getCurrentView(), preset.getArguments(), Credentials.empty()); }); presetsMenu.add(item); } } - cancelAndRunInteractive(action, frame.getCurrentView(), arguments, + runAction(action, frame.getCurrentView(), arguments, Credentials.empty()); } @@ -433,24 +428,8 @@ public class WebServicesMenuManager } } - 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 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, + private void runAction(ActionI action, AlignmentViewport viewport, List args, Credentials credentials) { // casting and instance checks can be avoided with some effort, @@ -460,16 +439,53 @@ public class WebServicesMenuManager // TODO: test if selection contains enough sequences var _action = (AlignmentAction) action; var handler = new AlignmentServiceGuiHandler(_action, frame); - return _action.perform(viewport, args, credentials, handler); + BaseTask task = _action.createTask(viewport, args, credentials); + var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor()); + task.addTaskEventListener(handler); + var future = executor.submit(task); + task.setCancelAction(() -> { future.cancel(true); }); + return; } if (action instanceof AnnotationAction) { + var calcManager = viewport.getCalcManager(); + var _action = (AnnotationAction) action; + var worker = new AlignCalcWorkerAdapter(viewport, frame.alignPanel, + _action, args, credentials); var handler = new AnnotationServiceGuiHandler(_action, frame); - return _action.perform(viewport, args, credentials, handler); + worker.setWorkerListener(handler); + for (var w : calcManager.getWorkers()) + { + if (worker.getCalcName() != null && worker.getCalcName().equals(w.getCalcName())) + { + calcManager.cancelWorker(w); + calcManager.removeWorker(w); + } + } + if (action.getWebService().isInteractive()) + calcManager.registerWorker(worker); + else + calcManager.startWorker(worker); + return; + } + if (action instanceof PhmmerAction) + { + var _action = (PhmmerAction) action; + var handler = new SearchServiceGuiHandler(_action, frame); + TaskI task = _action.createTask(viewport, args, credentials); + var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor()); + task.addTaskEventListener(handler); + _action.perform(viewport, args, credentials, handler); + return; } - throw new IllegalArgumentException( - String.format("Illegal action type %s", action.getClass().getName())); + Console.warn(String.format( + "No known handler for action type %s. All output will be discarded.", + action.getClass().getName())); + var task = action.createTask(viewport, args, credentials); + task.addTaskEventListener(TaskEventListener.nullListener()); + PollingTaskExecutor.fromPool(viewport.getServiceExecutor()) + .submit(task); } private static CompletionStage> openEditParamsDialog( diff --git a/src/jalview/ws2/helpers/TaskEventSupport.java b/src/jalview/ws2/helpers/TaskEventSupport.java index c7b6052..dcf965a 100644 --- a/src/jalview/ws2/helpers/TaskEventSupport.java +++ b/src/jalview/ws2/helpers/TaskEventSupport.java @@ -1,6 +1,7 @@ package jalview.ws2.helpers; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import jalview.ws2.actions.api.JobI; import jalview.ws2.actions.api.TaskEventListener; @@ -9,52 +10,70 @@ import jalview.ws2.api.JobStatus; public class TaskEventSupport { - TaskI source; - TaskEventListener handler; - - public TaskEventSupport(TaskI source, TaskEventListener handler) + private TaskI source; + + private List> listeners = new CopyOnWriteArrayList<>(); + + public TaskEventSupport(TaskI source) { this.source = source; - this.handler = handler; + } + + public TaskEventSupport(TaskI source, TaskEventListener handler) + { + this(source); + addListener(handler); + } + + public void addListener(TaskEventListener listener) + { + listeners.add(listener); } + public void removeListener(TaskEventListener listener) + { + listeners.remove(listener); + } + public void fireTaskStarted(List subJobs) { - handler.taskStarted(source, subJobs); + for (var listener : listeners) + listener.taskStarted(source, subJobs); } - + public void fireTaskStatusChanged(JobStatus status) { - handler.taskStatusChanged(source, status); + for (var listener : listeners) + listener.taskStatusChanged(source, status); } - + public void fireTaskCompleted(T result) { - handler.taskCompleted(source, result); + for (var listener : listeners) + listener.taskCompleted(source, result); } - + public void fireTaskException(Exception e) { - handler.taskException(source, e); - } - - public void fireTaskRestarted() - { - handler.taskRestarted(source); + for (var listener : listeners) + listener.taskException(source, e); } - + public void fireSubJobStatusChanged(JobI job, JobStatus status) { - handler.subJobStatusChanged(source, job, status); + for (var listener : listeners) + listener.subJobStatusChanged(source, job, status); } - + public void fireSubJobLogChanged(JobI job, String log) { - handler.subJobLogChanged(source, job, log); + for (var listener : listeners) + listener.subJobLogChanged(source, job, log); } - + public void fireSubJobErrorLogChanged(JobI job, String log) { - handler.subJobErrorLogChanged(source, job, log); + for (var listener : listeners) + listener.subJobErrorLogChanged(source, job, log); } } diff --git a/test/jalview/ws2/actions/alignment/AlignmentActionTest.java b/test/jalview/ws2/actions/alignment/AlignmentActionTest.java index 5586108..e573032 100644 --- a/test/jalview/ws2/actions/alignment/AlignmentActionTest.java +++ b/test/jalview/ws2/actions/alignment/AlignmentActionTest.java @@ -24,6 +24,7 @@ import jalview.datamodel.SequenceI; import jalview.gui.AlignViewport; import jalview.viewmodel.AlignmentViewport; import jalview.ws.params.ParamDatastoreI; +import jalview.ws2.actions.PollingTaskExecutor; import jalview.ws2.actions.api.JobI; import jalview.ws2.actions.api.TaskEventListener; import jalview.ws2.api.Credentials; @@ -210,12 +211,16 @@ public class AlignmentActionTest return null; }) .when(listener).taskCompleted(any(), any()); - action.perform(viewport, List.of(), Credentials.empty(), listener); + var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor()); + var task = action.createTask(viewport, List.of(), Credentials.empty()); + task.addTaskEventListener(listener); + var cancellable = executor.submit(task); try { latch.await(100, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { + cancellable.cancel(true); } return listener; }