--- /dev/null
+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<T extends BaseJob, R> implements TaskI<R>
+{
+ protected final long uid = MathUtils.getUID();
+
+ protected final WebServiceClientI webClient;
+
+ protected final List<ArgumentI> args;
+
+ protected final Credentials credentials;
+
+ private final TaskEventSupport<R> eventHandler;
+
+ protected JobStatus status = JobStatus.CREATED;
+
+ protected List<T> jobs = Collections.emptyList();
+
+ protected R result = null;
+
+ protected Runnable cancelAction = () -> {
+ };
+
+ protected BaseTask(WebServiceClientI webClient, List<ArgumentI> 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<? extends BaseJob> getSubJobs()
+ {
+ return jobs;
+ }
+
+ @Override
+ public final void addTaskEventListener(TaskEventListener<R> listener)
+ {
+ eventHandler.addListener(listener);
+ }
+
+ @Override
+ public final void removeTaskEventListener(TaskEventListener<R> 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<T> 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<T> 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<T> prepareJobs() throws ServiceInputInvalidException;
+
+ protected abstract R collectResult(List<T> jobs) throws IOException;
+
+ /**
+ * Update task status according to the overall status of its jobs. The rules
+ * of setting the status are following:
+ * <ul>
+ * <li>task is invalid if all jobs are invalid</li>
+ * <li>task is completed if all but invalid jobs are completed</li>
+ * <li>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.</li>
+ * <li>task is running if at least one job is running and none are failed or
+ * cancelled</li>
+ * <li>task is cancelled if at least one job is cancelled and none failed</li>
+ * <li>task is failed or server error if at least one job is failed or server
+ * error</li>
+ * </ul>
+ */
+ 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);
+ }
+}
--- /dev/null
+ 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.TaskEventListener;
+ 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<Void>
+ {
+ public static final class Builder extends BaseAction.Builder<NullAction>
+ {
+ public NullAction build()
+ {
+ return new NullAction(this);
+ }
+ }
+
+ public static Builder newBuilder()
+ {
+ return new Builder();
+ }
+
+ protected NullAction(Builder builder)
+ {
+ super(builder);
+ }
+
+ @Override
- public TaskI<Void> perform(AlignmentViewport viewport,
- List<ArgumentI> args, Credentials credentials,
- TaskEventListener<Void> handler)
++ public TaskI<Void> createTask(AlignViewportI viewport,
++ List<ArgumentI> args, Credentials credentials)
+ {
+ return new NullTask();
+ }
+
+ @Override
+ public boolean isActive(AlignmentViewport viewport)
+ {
+ return false;
+ }
+ }
--- /dev/null
+ 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.
++ * An empty task returned by the {@link NullAction}. Use as a placeholder for
++ * testing purposes.
+ *
+ * @author mmwarowny
+ *
+ */
+ class NullTask implements TaskI<Void>
+ {
+ @Override
+ public long getUid()
+ {
+ return 0;
+ }
+
+ @Override
+ public JobStatus getStatus()
+ {
+ return JobStatus.READY;
+ }
+
+ @Override
+ public List<? extends JobI> 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<Void> listener)
++ {
++ }
++
++ @Override
++ public void removeTaskEventListener(TaskEventListener<Void> listener)
++ {
++ }
+ }
* @param log
* new log string
*/
- void subJobErrorLogChanged(TaskI<T> source, JobI job, String log);
+ default void subJobErrorLogChanged(TaskI<T> 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 taskRestarted(TaskI source)
- {
- Console.info("task restarted");
- }
-
- @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 <T> TaskEventListener<T> nullListener()
+ {
+ return (TaskEventListener<T>) NULL_LISTENER;
+ }
}
--- /dev/null
+ 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<AlignmentI>
+ {
+ public static class Builder extends BaseAction.Builder<PhmmerAction>
+ {
+ 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;
+ }
+
- @Override
+ public TaskI<AlignmentI> perform(AlignmentViewport viewport,
+ List<ArgumentI> args, Credentials credentials,
+ TaskEventListener<AlignmentI> handler)
+ {
- var task = new PhmmerTask(client, args, credentials,
- viewport.getAlignmentView(true), handler);
- task.start(viewport.getServiceExecutor());
++ 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<ArgumentI> args, Credentials credentials)
++ {
++ return new PhmmerTask(client, args, credentials, viewport.getAlignmentView(true));
++ }
+
+ @Override
+ public boolean isActive(AlignmentViewport viewport)
+ {
+ return false;
+ }
+ }
--- /dev/null
+ package jalview.ws2.actions.hmmer;
+
+ import static jalview.util.Comparison.GapChars;
+
+ import java.io.IOException;
-import java.util.Arrays;
+ 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.AbstractPollableTask;
+ import jalview.ws2.actions.BaseJob;
++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;
+
-class PhmmerTask extends AbstractPollableTask<BaseJob, AlignmentI>
++class PhmmerTask extends BaseTask<BaseJob, AlignmentI>
+ {
+ private final AlignmentWebServiceClientI client;
+ private final AlignmentView view;
+
+ PhmmerTask(AlignmentWebServiceClientI client, List<ArgumentI> args,
- Credentials credentials, AlignmentView view,
- TaskEventListener<AlignmentI> eventListener)
++ Credentials credentials, AlignmentView view)
+ {
- super(client, args, credentials, eventListener);
++ super(client, args, credentials);
+ this.client = client;
+ this.view = view;
+ }
+
+ @Override
- protected List<BaseJob> prepare() throws ServiceInputInvalidException
++ protected List<BaseJob> 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 done() throws IOException
++ protected AlignmentI collectResult(List<BaseJob> jobs) throws IOException
+ {
- var job = getSubJobs().get(0);
++ 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;
+ }
+ }
--- /dev/null
+ 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<WebService<?>> 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);
- // TODO change once a concrete action is implemented
+ var wsBuilder = WebService.<PhmmerAction> 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);
+ }
+
+ }
--- /dev/null
+ 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<AlignmentI>
+ {
+ 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<AlignmentI> source,
+ List<? extends JobI> 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<AlignmentI> 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<AlignmentI> 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<AlignmentI> source, Exception e)
+ {
+ Console.error(format("Task %s#%x raised an exception.",
+ service.getName(), source.getUid()), e);
+ infoPanel.appendProgressText(e.getMessage());
+ }
+
+ @Override
- public void taskRestarted(TaskI<AlignmentI> source)
- {
- // search services non-restartable
- }
-
- @Override
+ public void subJobStatusChanged(TaskI<AlignmentI> 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<AlignmentI> 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<AlignmentI> 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]));
+ }
+ }
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;
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;
}
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;
}
- throw new IllegalArgumentException(
- String.format("Illegal action type %s", action.getClass().getName()));
+ if (action instanceof PhmmerAction)
+ {
+ var _action = (PhmmerAction) action;
+ var handler = new SearchServiceGuiHandler(_action, frame);
- return _action.perform(viewport, args, credentials, handler);
++ TaskI<AlignmentI> task = _action.createTask(viewport, args, credentials);
++ var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
++ task.addTaskEventListener(handler);
++ _action.perform(viewport, args, credentials, handler);
++ return;
+ }
+ Console.warn(String.format(
+ "No known handler for action type %s. All output will be discarded.",
+ action.getClass().getName()));
- return action.perform(viewport, args, credentials,
- TaskEventListener.nullListener());
++ var task = action.createTask(viewport, args, credentials);
++ task.addTaskEventListener(TaskEventListener.nullListener());
++ PollingTaskExecutor.fromPool(viewport.getServiceExecutor())
++ .submit(task);
}
private static CompletionStage<List<ArgumentI>> openEditParamsDialog(