* @return
*/
boolean isDeletable();
+
+ /**
+ * Returns the name of this calculation.
+ */
+ public default String getCalcName()
+ {
+ return null;
+ }
}
*/
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;
+ }
+
}
*/
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<? extends ContiguousI> 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;
+ }
}
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();
RemoteFile featFile = null;
try
{
- var slivkaJob = client.getJob(jobId.getJobId());
- Collection<RemoteFile> files = slivkaJob.getResults();
+ Collection<RemoteFile> files = client.fetchFilesList(jobId.getJobId());
for (RemoteFile f : files)
{
if (f.getMediaType().equals("application/jalview-annotations"))
Collection<RemoteFile> files;
try
{
- var slivkaJob = client.getJob(jobId.getJobId());
- files = slivkaJob.getResults();
+ files = client.fetchFilesList(jobId.getJobId());
for (RemoteFile f : files)
{
if (f.getMediaType().equals("application/clustal"))
for (String url : getServiceUrls())
{
- SlivkaClient client = new SlivkaClient(url);
+ SlivkaClient client = SlivkaClient.newInstance(url);
List<SlivkaService> services;
try
{
try
{
- List<?> services = new SlivkaClient(url).getServices();
+ List<?> services = SlivkaClient.newInstance(url).getServices();
return services.isEmpty() ? STATUS_NO_SERVICES : STATUS_OK;
} catch (IOException | org.json.JSONException e)
{
import java.util.Collection;
import uk.ac.dundee.compbio.slivkaclient.Job;
-import uk.ac.dundee.compbio.slivkaclient.JobRequest;
+import uk.ac.dundee.compbio.slivkaclient.RequestValues;
import uk.ac.dundee.compbio.slivkaclient.Parameter;
import uk.ac.dundee.compbio.slivkaclient.RemoteFile;
import uk.ac.dundee.compbio.slivkaclient.SlivkaClient;
WsParamSetI preset, List<ArgumentI> args) throws Throwable
{
var parameters = service.getParameters();
- var request = new JobRequest();
+ var request = new RequestValues();
for (Parameter param : parameters)
{
if (param instanceof Parameter.FileParameter)
}
}
}
- var job = service.submitJob(request);
- return new JobId(service.getName(), service.getName(), job.getId());
+ var jobId = client.submitJob(service, request);
+ return new JobId(service.getName(), service.getName(), jobId);
}
@Override
{
try
{
- var slivkaJob = client.getJob(job.getJobId());
- job.setState(stateMap.get(slivkaJob.getStatus()));
+ job.setState(stateMap.get(client.fetchJobStatus(job.getJobId())));
} catch (IOException e)
{
throw new IOError(e);
@Override
public final boolean updateJobProgress(WsJob job) throws IOException
{
- var slivkaJob = client.getJob(job.getJobId());
- Collection<RemoteFile> files = slivkaJob.getResults();
+ Collection<RemoteFile> files = client.fetchFilesList(job.getJobId());
RemoteFile logFile=null;
for (RemoteFile f : files)
{
if (logFile!=null)
{
ByteArrayOutputStream output = new ByteArrayOutputStream();
- logFile.writeTo(output);
+ client.writeFileTo(logFile, output);
if (output.size() > job.getNextChunk())
{
newContent = true;
if (errLogFile!=null)
{
ByteArrayOutputStream output = new ByteArrayOutputStream();
- errLogFile.writeTo(output);
+ client.writeFileTo(errLogFile, output);
if (output.size() > 0)
{
newContent = true;
+++ /dev/null
-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 <T>
- * the type of jobs managed by the task
- * @param <R>
- * the type of result provided by the task
- */
-public abstract class AbstractPollableTask<T extends BaseJob, R> implements TaskI<R>
-{
- private final long uid = MathUtils.getUID();
-
- protected final WebServiceClientI client;
-
- protected final List<ArgumentI> args;
-
- protected final Credentials credentials;
-
- private final TaskEventSupport<R> eventHandler;
-
- protected JobStatus taskStatus = null;
-
- private Future<?> future = null;
-
- protected List<T> jobs = Collections.emptyList();
-
- protected R result;
-
- protected AbstractPollableTask(WebServiceClientI client, List<ArgumentI> args,
- Credentials credentials, TaskEventListener<R> eventListener)
- {
- this.client = client;
- this.args = args;
- this.credentials = credentials;
- this.eventHandler = new TaskEventSupport<R>(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:
- * <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>
- */
- 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<? extends BaseJob> 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<T> 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);
- }
-}
protected final List<SequenceI> inputSeqs;
- protected JobStatus status = null;
+ protected JobStatus status = JobStatus.CREATED;
protected String log = "";
* @param listener
* property change listener
*/
- public final void addPropertyChagneListener(PropertyChangeListener listener)
+ public final void addPropertyChangeListener(PropertyChangeListener listener)
{
pcs.addPropertyChangeListener(listener);
}
--- /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);
+ }
+}
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;
}
@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();
}
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
*
}
@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;
public void cancel()
{
}
+
+ @Override
+ public void addTaskEventListener(TaskEventListener<Void> listener)
+ {
+ }
+
+ @Override
+ public void removeTaskEventListener(TaskEventListener<Void> listener)
+ {
+ }
}
\ No newline at end of file
--- /dev/null
+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<ScheduledExecutorService, PollingTaskExecutor> 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();
+ }
+ }
+ }
+}
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;
}
@Override
- public TaskI<AlignmentResult> perform(AlignmentViewport viewport,
- List<ArgumentI> args, Credentials credentials,
- TaskEventListener<AlignmentResult> handler)
+ public AlignmentTask createTask(AlignViewportI viewport,
+ List<ArgumentI> 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);
}
/**
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;
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;
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;
* @author mmwarowny
*
*/
-class AlignmentTask extends AbstractPollableTask<AlignmentJob, AlignmentResult>
+class AlignmentTask extends BaseTask<AlignmentJob, AlignmentResult>
{
/* task parameters set in the constructor */
private final AlignmentWebServiceClientI client;
private final AlignmentView msa; // a.k.a. input
- private final AlignViewportI viewport;
-
private final boolean submitGaps;
private final AlignmentI currentView;
AlignmentTask(AlignmentWebServiceClientI client, AlignmentAction action,
List<ArgumentI> args, Credentials credentials,
- AlignmentView msa, AlignViewportI viewport, boolean submitGaps,
- TaskEventListener<AlignmentResult> 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();
}
@Override
- protected List<AlignmentJob> prepare() throws ServiceInputInvalidException
+ protected List<AlignmentJob> prepareJobs() throws ServiceInputInvalidException
{
Console.info(format("starting alignment service %s:%s",
client.getClientName(), action.getName()));
}
@Override
- protected AlignmentResult done() throws IOException
+ protected AlignmentResult collectResult(List<AlignmentJob> jobs) throws IOException
{
IOException lastIOE = null;
for (AlignmentJob job : jobs)
--- /dev/null
+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<ArgumentI> args;
+
+ private final Credentials credentials;
+
+ private TaskI<AnnotationResult> currentTask = null;
+
+ private TaskEventListener<AnnotationResult> taskListener = new TaskEventListener<>()
+ {
+ @Override
+ public void taskCompleted(TaskI source, AnnotationResult result)
+ {
+ int graphGroup = alignViewport.getAlignment().getLastGraphGroup();
+ List<AlignmentAnnotation> 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<ArgumentI> 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<AlignmentAnnotation> newAnnots)
+ {
+ List<AlignmentAnnotation> 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;
+ }
+}
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;
requireAlignedSequences = builder.requireAlignedSequences;
filterSymbols = builder.filterSymbols;
}
-
+
@Override
- public TaskI<AnnotationResult> perform(AlignmentViewport viewport,
- List<ArgumentI> args, Credentials credentials,
- TaskEventListener<AnnotationResult> handler)
+ public AnnotationTask createTask(AlignViewportI viewport,
+ List<ArgumentI> 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.
*/
* @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
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;
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<AnnotationResult>
+public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
{
- private final long uid = MathUtils.getUID();
-
private AnnotationWebServiceClientI client;
private final AnnotationAction action;
- private final List<ArgumentI> args;
-
- private final Credentials credentials;
-
- private final AlignViewportI viewport;
-
- private final TaskEventSupport<AnnotationResult> eventHandler;
-
- private JobStatus taskStatus = null;
-
- private AlignCalcWorkerAdapter worker = null;
-
- private List<AnnotationJob> jobs = Collections.emptyList();
-
- private AnnotationResult result = null;
-
- private DelegateJobEventListener<AnnotationResult> 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<AlignmentAnnotation> newAnnots)
- {
- List<AlignmentAnnotation> 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<ArgumentI> args, Credentials credentials,
- AlignViewportI viewport,
- TaskEventListener<AnnotationResult> 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<AlignCalcWorkerI> 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<? extends JobI> getSubJobs()
- {
- return jobs;
+ this.alignment = viewport.getAlignment();
+ this.selectionGroup = viewport.getSelectionGroup();
}
/**
* @throws ServiceInputInvalidException
* input data is not valid
*/
- private List<AnnotationJob> prepare() throws ServiceInputInvalidException
+ @Override
+ public List<AnnotationJob> prepareJobs() throws ServiceInputInvalidException
{
- AlignmentI alignment = viewport.getAlignment();
if (alignment == null || alignment.getWidth() <= 0 ||
alignment.getSequences() == null)
throw new ServiceInputInvalidException("Alignment does not contain sequences");
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;
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<AnnotationJob> jobs) throws IOException
{
final Map<String, FeatureColourI> featureColours = new HashMap<>();
final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
* 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<List<AlignmentAnnotation>, Boolean> updateResultAnnotation(
- AnnotationJob job, List<AlignmentAnnotation> annotations)
- {
- List<AlignmentAnnotation> 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<ContiguousI> 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<ContiguousI> 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<AlignmentAnnotation> 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<ContiguousI> 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<ContiguousI> 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;
}
}
import javax.swing.Icon;
+import jalview.api.AlignViewportI;
import jalview.viewmodel.AlignmentViewport;
import jalview.ws.params.ArgumentI;
import jalview.ws2.api.CredentialType;
EnumSet<CredentialType> 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
* 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<R> perform(AlignmentViewport viewport, List<ArgumentI> args,
- Credentials credentials, TaskEventListener<R> handler);
+ TaskI<R> createTask(AlignViewportI viewport, List<ArgumentI> args,
+ Credentials credentials);
/**
* Return if the action is currently active for the given viewport. Active
* @param subJobs
* list of sub-jobs for this run
*/
- void taskStarted(TaskI<T> source, List<? extends JobI> subJobs);
+ default void taskStarted(TaskI<T> source, List<? extends JobI> subJobs) {};
/**
* Invoked when the global task status has changed.
* @param status
* new task status
*/
- void taskStatusChanged(TaskI<T> source, JobStatus status);
+ default void taskStatusChanged(TaskI<T> source, JobStatus status) {};
/**
* Invoked when the task has completed. If the task completed with a result,
* @param result
* computation result or null if result not present
*/
- void taskCompleted(TaskI<T> source, T result);
+ default void taskCompleted(TaskI<T> source, T result) {};
/**
* Invoked when an unhandled exception has occurred during task execution.
* @param e
* exception
*/
- void taskException(TaskI<T> 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<T> source);
+ default void taskException(TaskI<T> source, Exception e) {};
/**
* Invoked when the status of a sub-job has changed.
* @param status
* new job status
*/
- void subJobStatusChanged(TaskI<T> source, JobI job, JobStatus status);
+ default void subJobStatusChanged(TaskI<T> source, JobI job, JobStatus status) {};
/**
* Invoked when a log string of the sub-job has changed.
* @param log
* new log string
*/
- void subJobLogChanged(TaskI<T> source, JobI job, String log);
+ default void subJobLogChanged(TaskI<T> source, JobI job, String log) {};
/**
* Invoked when an error log string of the sub-job has changed.
* @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 taskRestarted(TaskI source)
- {
- Console.info("task restarted");
- }
-
- @Override
public void subJobStatusChanged(TaskI source, JobI job,
JobStatus status)
{
*/
List<? extends JobI> getSubJobs();
+ void addTaskEventListener(TaskEventListener<T> listener);
+
+ void removeTaskEventListener(TaskEventListener<T> 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.
*/
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.
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;
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)
import static jalview.util.Comparison.GapChars;
import java.io.IOException;
-import java.util.Arrays;
import java.util.List;
import jalview.analysis.AlignSeq;
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);
}
@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));
public enum JobStatus
{
+ /** Initial status before the job is started. */
+ CREATED,
/** Job has invalid inputs and cannot be started. */
INVALID,
/** Job is created and ready for submission. */
case CANCELLED:
case SERVER_ERROR:
return true;
+ case CREATED:
case READY:
case SUBMITTED:
case QUEUED:
JobStatus.UNKNOWN, // unknown prevents successful completion but not
// running or failure
JobStatus.READY,
+ JobStatus.CREATED,
JobStatus.SUBMITTED,
JobStatus.QUEUED,
JobStatus.RUNNING,
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");
final SlivkaClient client;
- SlivkaWSClient(SlivkaService service)
+ SlivkaWSClient(SlivkaClient client, SlivkaService service)
{
this.service = service;
- this.client = service.getClient();
+ this.client = client;
}
@Override
public WebServiceJobHandle submit(List<SequenceI> sequences,
List<ArgumentI> args, Credentials credentials) throws IOException
{
- var request = new uk.ac.dundee.compbio.slivkaclient.JobRequest();
+ var request = new uk.ac.dundee.compbio.slivkaclient.RequestValues();
for (Parameter param : service.getParameters())
{
// TODO: restrict input sequences parameter name to "sequences"
}
}
}
- var job = service.submitJob(request);
- return createJobHandle(job.getId());
+ var jobId = client.submitJob(service, request);
+ return createJobHandle(jobId);
}
protected WebServiceJobHandle createJobHandle(String jobId)
@Override
public JobStatus getStatus(WebServiceJobHandle job) throws IOException
{
- var slivkaJob = client.getJob(job.getJobId());
- return statusMap.getOrDefault(slivkaJob.getStatus(), JobStatus.UNKNOWN);
+ return statusMap.getOrDefault(client.fetchJobStatus(job.getJobId()), JobStatus.UNKNOWN);
}
protected static final EnumMap<Job.Status, JobStatus> statusMap = new EnumMap<>(Job.Status.class);
@Override
public String getLog(WebServiceJobHandle job) throws IOException
{
- var slivkaJob = client.getJob(job.getJobId());
- for (var f : slivkaJob.getResults())
+ for (var f : client.fetchFilesList(job.getJobId()))
{
if (f.getLabel().equals("log"))
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
- f.writeTo(stream);
+ client.writeFileTo(f, stream);
return stream.toString("UTF-8");
}
}
@Override
public String getErrorLog(WebServiceJobHandle job) throws IOException
{
- var slivkaJob = client.getJob(job.getJobId());
- for (var f : slivkaJob.getResults())
+ for (var f : client.fetchFilesList(job.getJobId()))
{
if (f.getLabel().equals("error-log"))
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
- f.writeTo(stream);
+ client.writeFileTo(f, stream);
return stream.toString("UTF-8");
}
}
implements AlignmentWebServiceClientI
{
- SlivkaAlignmentWSClient(SlivkaService service)
+ SlivkaAlignmentWSClient(SlivkaClient client, SlivkaService service)
{
- super(service);
+ super(client, service);
}
@Override
public AlignmentI getAlignment(WebServiceJobHandle job) throws IOException
{
- var slivkaJob = client.getJob(job.getJobId());
- for (var f : slivkaJob.getResults())
+ for (var f : client.fetchFilesList(job.getJobId()))
{
// TODO: restrict result file label to "alignment"
FileFormat format;
class SlivkaAnnotationWSClient extends SlivkaWSClient
implements AnnotationWebServiceClientI
{
- SlivkaAnnotationWSClient(SlivkaService service)
+ SlivkaAnnotationWSClient(SlivkaClient client, SlivkaService service)
{
- super(service);
+ super(client, service);
}
@Override
List<SequenceI> sequences, Map<String, FeatureColourI> colours,
Map<String, FeatureMatcherSetI> filters) throws IOException
{
- var slivkaJob = client.getJob(job.getJobId());
var aln = new Alignment(sequences.toArray(new SequenceI[sequences.size()]));
boolean featPresent = false, annotPresent = false;
- for (var f : slivkaJob.getResults())
+ for (var f : client.fetchFilesList(job.getJobId()))
{
// TODO: restrict file label to "annotations" or "features"
var match = mediaTypePattern.matcher(f.getMediaType());
{
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()));
}
import java.io.IOException;
import java.net.MalformedURLException;
+import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.Function;
import jalview.bin.Cache;
import jalview.bin.Console;
private static SlivkaWSDiscoverer instance = null;
private static ParamManager paramManager = null;
+
+ private final Function<URI, SlivkaClient> clientFactory;
- private SlivkaWSDiscoverer()
+ SlivkaWSDiscoverer(Function<URI, SlivkaClient> clientFactory)
{
+ this.clientFactory = clientFactory;
}
public static SlivkaWSDiscoverer getInstance()
{
if (instance == null)
- instance = new SlivkaWSDiscoverer();
+ instance = new SlivkaWSDiscoverer(SlivkaClient::newInstance);
return instance;
}
{
try
{
- List<?> services = new SlivkaClient(url.toString()).getServices();
+ List<?> services = clientFactory.apply(url.toURI()).getServices();
return services.isEmpty() ? STATUS_NO_SERVICES : STATUS_OK;
+ } catch (URISyntaxException e)
+ {
+ Console.error("invalid URL " + url, e);
+ return STATUS_INVALID;
} catch (IOException e)
{
Console.error("slivka could not retrieve services from " + url, e);
SlivkaClient slivkaClient;
try
{
- slivkaClient = new SlivkaClient(url.toURI());
+ slivkaClient = clientFactory.apply(url.toURI());
} catch (URISyntaxException e)
{
throw new MalformedURLException(e.getMessage());
if (serviceClass == SERVICE_CLASS_MSA)
{
var wsb = WebService.<AlignmentAction> newBuilder();
- initServiceBuilder(slivkaService, wsb);
+ initServiceBuilder(slivkaClient, slivkaService, wsb);
wsb.category("Alignment");
wsb.interactive(false);
wsb.actionClass(AlignmentAction.class);
var msaService = wsb.build();
boolean canRealign = msaService.getName().contains("lustal");
- var client = new SlivkaAlignmentWSClient(slivkaService);
+ var client = new SlivkaAlignmentWSClient(slivkaClient, slivkaService);
var actionBuilder = AlignmentAction.newBuilder(client);
actionBuilder.name("Alignment");
actionBuilder.webService(msaService);
else if (serviceClass == SERVICE_CLASS_PROT_SEQ_ANALYSIS)
{
var wsb = WebService.<AnnotationAction> newBuilder();
- initServiceBuilder(slivkaService, wsb);
+ initServiceBuilder(slivkaClient, slivkaService, wsb);
wsb.category("Protein Disorder");
wsb.interactive(false);
wsb.actionClass(AnnotationAction.class);
var psaService = wsb.build();
- var client = new SlivkaAnnotationWSClient(slivkaService);
+ var client = new SlivkaAnnotationWSClient(slivkaClient, slivkaService);
var actionBuilder = AnnotationAction.newBuilder(client);
actionBuilder.webService(psaService);
actionBuilder.name("Analysis");
else if (serviceClass == SERVICE_CLASS_CONSERVATION)
{
var wsb = WebService.<AnnotationAction> newBuilder();
- initServiceBuilder(slivkaService, wsb);
+ initServiceBuilder(slivkaClient, slivkaService, wsb);
wsb.category("Conservation");
wsb.interactive(true);
wsb.actionClass(AnnotationAction.class);
var conService = wsb.build();
- var client = new SlivkaAnnotationWSClient(slivkaService);
+ var client = new SlivkaAnnotationWSClient(slivkaClient, slivkaService);
var actionBuilder = AnnotationAction.newBuilder(client);
actionBuilder.webService(conService);
actionBuilder.name("");
else if (serviceClass == SERVICE_CLASS_RNA_SEC_STR_PRED)
{
var wsb = WebService.<AnnotationAction> newBuilder();
- initServiceBuilder(slivkaService, wsb);
+ initServiceBuilder(slivkaClient, slivkaService, wsb);
wsb.category("Secondary Structure Prediction");
wsb.interactive(true);
wsb.actionClass(AnnotationAction.class);
var predService = wsb.build();
- var client = new SlivkaAnnotationWSClient(slivkaService);
+ var client = new SlivkaAnnotationWSClient(slivkaClient, slivkaService);
var actionBuilder = AnnotationAction.newBuilder(client);
actionBuilder.webService(predService);
actionBuilder.name("Prediction");
return allServices;
}
- private void initServiceBuilder(SlivkaService service, WebService.Builder<?> wsBuilder)
+ private void initServiceBuilder(SlivkaClient client, SlivkaService service, WebService.Builder<?> wsBuilder)
{
try
{
- wsBuilder.url(service.getClient().getUrl().toURL());
+ wsBuilder.url(client.getUrl().toURL());
} catch (MalformedURLException e)
{
e.printStackTrace();
for (String classifier : service.getClassifiers())
{
String[] path = classifier.split("\\s*::\\s*");
- if (path.length < 3 || !path[0].equalsIgnoreCase("operation") ||
- !path[1].equalsIgnoreCase("analysis"))
+ if (path.length < 3 || !path[0].equalsIgnoreCase("operation"))
continue;
- // classifier is operation :: analysis :: *
+ // classifier is operation :: *
var tail = path[path.length - 1].toLowerCase();
switch (tail)
{
}
@Override
- public void taskRestarted(TaskI<AlignmentResult> source)
- {
- // alignment services are not restartable
- }
-
- @Override
public void subJobStatusChanged(TaskI<AlignmentResult> source, JobI job, JobStatus status)
{
int i = ArrayUtils.indexOf(jobs, job);
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<AnnotationResult>
+ implements AlignCalcWorkerAdapter.WorkerListener
{
+ private final long progressId = MathUtils.getUID();
+
private final AlignFrame alignFrame;
private final AlignmentPanel alignPanel;
}
@Override
- public void taskStarted(TaskI<AnnotationResult> source, List<? extends JobI> subJobs)
+ public void workerStarted(AlignCalcWorkerAdapter source)
{
- progressIndicator.registerHandler(source.getUid(),
+ progressIndicator.addProgressBar(progressId, action.getFullName());
+ progressIndicator.registerHandler(progressId,
new IProgressIndicatorHandler()
{
@Override
}
@Override
- public void taskStatusChanged(TaskI<AnnotationResult> 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<AnnotationResult> 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<AnnotationResult> source, Exception e)
- {
-
- }
-
- @Override
- public void taskRestarted(TaskI<AnnotationResult> source)
- {
-
- }
-
- @Override
- public void subJobStatusChanged(TaskI<AnnotationResult> source, JobI job, JobStatus status)
- {
-
- }
-
- @Override
- public void subJobLogChanged(TaskI<AnnotationResult> 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<AnnotationResult> 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();
}
}
}
@Override
- public void taskRestarted(TaskI<AlignmentI> source)
- {
- // search services non-restartable
- }
-
- @Override
public void subJobStatusChanged(TaskI<AlignmentI> source, JobI job,
JobStatus status)
{
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;
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.api.WebService;
import jalview.ws2.client.api.WebServiceProviderI;
-import static java.lang.String.format;
-
public class WebServicesMenuManager
{
private final JMenu menu;
noServicesItem.setEnabled(false);
}
- private Map<String, WeakReference<TaskI<?>>> interactiveTasks = new HashMap<>();
-
public WebServicesMenuManager(String name, AlignFrame frame)
{
this.frame = frame;
// 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);
}
}
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);
lastPreset[0] = null;
arguments.clear();
arguments.addAll(args);
- cancelAndRunInteractive(action, frame.getCurrentView(),
+ runAction(action, frame.getCurrentView(),
arguments, Credentials.empty());
}
});
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());
}
}
}
- 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<ArgumentI> 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<ArgumentI> args, Credentials credentials)
{
// casting and instance checks can be avoided with some effort,
// 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<?, AlignmentResult> 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);
- 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(
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;
public class TaskEventSupport<T>
{
- TaskI<T> source;
- TaskEventListener<T> handler;
-
- public TaskEventSupport(TaskI<T> source, TaskEventListener<T> handler)
+ private TaskI<T> source;
+
+ private List<TaskEventListener<T>> listeners = new CopyOnWriteArrayList<>();
+
+ public TaskEventSupport(TaskI<T> source)
{
this.source = source;
- this.handler = handler;
+ }
+
+ public TaskEventSupport(TaskI<T> source, TaskEventListener<T> handler)
+ {
+ this(source);
+ addListener(handler);
+ }
+
+ public void addListener(TaskEventListener<T> listener)
+ {
+ listeners.add(listener);
}
+ public void removeListener(TaskEventListener<T> listener)
+ {
+ listeners.remove(listener);
+ }
+
public void fireTaskStarted(List<? extends JobI> 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);
}
}
--- /dev/null
+package jalview.ws2.actions.alignment;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.help.UnsupportedOperationException;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.mockito.ArgumentCaptor;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Sequence;
+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;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.api.WebService;
+import jalview.ws2.api.WebServiceJobHandle;
+import jalview.ws2.client.api.AlignmentWebServiceClientI;
+
+import org.mockito.hamcrest.MockitoHamcrest;
+import org.mockito.internal.hamcrest.HamcrestArgumentMatcher;
+
+import static org.mockito.Mockito.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+public class AlignmentActionTest
+{
+ protected AlignmentWebServiceClientI mockClient;
+
+ protected AlignmentAction.Builder actionBuilder;
+
+ protected WebServiceJobHandle jobRef;
+
+ @BeforeMethod
+ public void setupMockClient() throws IOException
+ {
+ jobRef = new WebServiceJobHandle(
+ "mock", "mock", "http://example.org", "00000001");
+ mockClient = mock(AlignmentWebServiceClientI.class);
+ when(mockClient.getUrl()).thenReturn("http://example.org");
+ when(mockClient.getClientName()).thenReturn("mock");
+ when(mockClient.submit(anyList(), anyList(), any())).thenReturn(jobRef);
+ when(mockClient.getLog(jobRef)).thenReturn("");
+ when(mockClient.getErrorLog(jobRef)).thenReturn("");
+ doThrow(new UnsupportedOperationException()).when(mockClient).cancel(any());
+ }
+
+ @BeforeMethod(dependsOnMethods = { "setupMockClient" })
+ public void setupActionBuilder() throws IOException
+ {
+ actionBuilder = AlignmentAction.newBuilder(mockClient);
+ actionBuilder.name("mock");
+ actionBuilder.webService(
+ WebService.<AlignmentAction> newBuilder()
+ .url(new URL("http://example.org"))
+ .clientName("mock")
+ .category("Alignment")
+ .name("mock")
+ .paramDatastore(mock(ParamDatastoreI.class))
+ .actionClass(AlignmentAction.class)
+ .build());
+ }
+
+ @DataProvider
+ public Object[][] multipleSequencesUnalignedAndAligned()
+ {
+ return new Object[][] {
+ {
+ new Alignment(new SequenceI[]
+ {
+ new Sequence("Seq 1", "----ASTVLITOPDCMMQEGGST-"),
+ new Sequence("Seq 2", "-ASCGLITO------MMQEGGST-"),
+ new Sequence("Seq 3", "AS--TVL--OPDTMMQEL------")
+ }),
+ new Alignment(new SequenceI[]
+ {
+ new Sequence("Sequence0", "ASTV-LITOPDCMMQEGGST----"),
+ new Sequence("Sequence1", "ASC-GLITO---MMQEGGST----"),
+ new Sequence("Sequence2", "ASTV-L--OPDTMMQE--L-----")
+ })
+ }
+ };
+ }
+
+ @Test(dataProvider = "multipleSequencesUnalignedAndAligned")
+ public void submitSequences_verifySequenceNamesUniquified(
+ Alignment unaligned, Alignment aligned)
+ throws IOException
+ {
+ var viewport = new AlignViewport(unaligned);
+ when(mockClient.getAlignment(jobRef)).thenReturn(aligned);
+ when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED);
+ actionBuilder.submitGaps(false);
+ performAction(viewport, actionBuilder.build());
+ ArgumentCaptor<List<SequenceI>> argument = ArgumentCaptor.forClass(List.class);
+ verify(mockClient).submit(argument.capture(), eq(List.of()), eq(Credentials.empty()));
+ assertThat(argument.getValue(),
+ contains(hasProperty("name", is("Sequence0")),
+ hasProperty("name", is("Sequence1")),
+ hasProperty("name", is("Sequence2"))));
+ }
+
+ @Test(dataProvider = "multipleSequencesUnalignedAndAligned")
+ public void submitSequences_submitGapsOff_verifySequencesSubmittedWithoutGaps(Alignment unaligned, Alignment aligned)
+ throws IOException
+ {
+ var viewport = new AlignViewport(unaligned);
+ actionBuilder.submitGaps(false);
+ when(mockClient.getAlignment(jobRef)).thenReturn(aligned);
+ when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED);
+ performAction(viewport, actionBuilder.build());
+ ArgumentCaptor<List<SequenceI>> argument = ArgumentCaptor.forClass(List.class);
+ verify(mockClient).submit(argument.capture(), eq(List.of()), eq(Credentials.empty()));
+ assertThat(argument.getValue(),
+ contains(
+ matchesSequence("ASTVLITOPDCMMQEGGST"),
+ matchesSequence("ASCGLITOMMQEGGST"),
+ matchesSequence("ASTVLOPDTMMQEL")));
+ }
+
+ @Test(dataProvider = "multipleSequencesUnalignedAndAligned")
+ public void submitSequences_submitGapsOn_verifySequencesSubmittedWithGaps(
+ Alignment unaligned, Alignment aligned)
+ throws IOException
+ {
+ var viewport = new AlignViewport(unaligned);
+ actionBuilder.submitGaps(true);
+ when(mockClient.getAlignment(jobRef)).thenReturn(aligned);
+ when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED);
+ performAction(viewport, actionBuilder.build());
+ ArgumentCaptor<List<SequenceI>> argument = ArgumentCaptor.forClass(List.class);
+ verify(mockClient).submit(argument.capture(), eq(List.of()), eq(Credentials.empty()));
+ assertThat(argument.getValue(),
+ contains(
+ matchesSequence("----ASTVLITOPDCMMQEGGST-"),
+ matchesSequence("-ASCGLITO------MMQEGGST-"),
+ matchesSequence("AS--TVL--OPDTMMQEL------")));
+ }
+
+ @Test(dataProvider = "multipleSequencesUnalignedAndAligned")
+ public void retrieveResult_verifySequencesAligned(
+ Alignment unaligned, Alignment aligned)
+ throws IOException
+ {
+ var viewport = new AlignViewport(unaligned);
+ actionBuilder.submitGaps(false);
+ when(mockClient.getAlignment(jobRef)).thenReturn(aligned);
+ when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED);
+ var mockListener = performAction(viewport, actionBuilder.build());
+ var argument = ArgumentCaptor.forClass(AlignmentResult.class);
+ verify(mockListener).taskCompleted(any(), argument.capture());
+ var alignmentResult = argument.getValue().getAlignment();
+ assertThat(alignmentResult, hasProperty("sequences", contains(
+ matchesSequence("ASTV-LITOPDCMMQEGGST----"),
+ matchesSequence("ASC-GLITO---MMQEGGST----"),
+ matchesSequence("ASTV-L--OPDTMMQE--L-----"))));
+ }
+
+ protected static Matcher<SequenceI> matchesSequence(String sequence)
+ {
+ return new TypeSafeMatcher<SequenceI>()
+ {
+ @Override
+ public boolean matchesSafely(SequenceI obj)
+ {
+ if (!(obj instanceof SequenceI))
+ return false;
+ var seq = (SequenceI) obj;
+ return seq.getSequenceAsString().equals(sequence);
+ }
+
+ @Override
+ public void describeTo(Description description)
+ {
+ description.appendText("a sequence ").appendValue(sequence);
+ }
+
+ @Override
+ public void describeMismatchSafely(SequenceI item, Description description)
+ {
+ description.appendText("was ").appendValue(item.getSequenceAsString());
+ }
+ };
+ }
+
+ protected TaskEventListener<AlignmentResult> performAction(
+ AlignmentViewport viewport, AlignmentAction action)
+ throws IOException
+ {
+ TaskEventListener<AlignmentResult> listener = mock(TaskEventListener.class);
+ var latch = new CountDownLatch(1);
+ doAnswer(invocation -> {
+ latch.countDown();
+ return null;
+ })
+ .when(listener).taskCompleted(any(), any());
+ 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;
+ }
+}
+
+class AlignmentActionListenerNotifiedTest extends AlignmentActionTest
+{
+ private AlignViewport viewport;
+
+ @BeforeMethod
+ public void setupViewport()
+ {
+ viewport = new AlignViewport(new Alignment(new SequenceI[] {
+ new Sequence("Seq 1", "----ASTVLITOPDCMMQEGGST-"),
+ new Sequence("Seq 2", "-ASCGLITO------MMQEGGST-"),
+ new Sequence("Seq 3", "AS--TVL--OPDTMMQEL------")
+ }));
+ }
+
+ @DataProvider
+ public JobStatus[] jobStatuses()
+ {
+ // CREATED, INVALID and READY should not be returned by the server
+ return new JobStatus[] {
+ JobStatus.SUBMITTED,
+ JobStatus.QUEUED,
+ JobStatus.RUNNING,
+ JobStatus.COMPLETED,
+ JobStatus.FAILED,
+ JobStatus.CANCELLED,
+ JobStatus.SERVER_ERROR,
+ JobStatus.UNKNOWN
+ };
+ }
+
+ @Test
+ public void allJobsStarted_taskStartedCalled()
+ throws IOException
+ {
+ when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED);
+ var mockListener = performAction(viewport, actionBuilder.build());
+ verify(mockListener).taskStarted(any(), anyList());
+ }
+
+ @Test
+ public void allJobsStarted_taskStatusChangedCalledWithReadyThenSubmitted()
+ throws IOException
+ {
+ when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED);
+ var mockListener = performAction(viewport, actionBuilder.build());
+ var inOrder = inOrder(mockListener);
+ inOrder.verify(mockListener).taskStatusChanged(any(), eq(JobStatus.READY));
+ inOrder.verify(mockListener).taskStatusChanged(any(), eq(JobStatus.SUBMITTED));
+ }
+
+ @Test(dataProvider = "jobStatuses")
+ public void jobStatusChanged_taskStatusChangedCalledWithJobStatus(JobStatus status)
+ throws IOException
+ {
+ when(mockClient.getStatus(jobRef))
+ .thenReturn(status)
+ .thenReturn(JobStatus.COMPLETED);
+ var mockListener = performAction(viewport, actionBuilder.build());
+ verify(mockListener).taskStatusChanged(any(), eq(status));
+ }
+
+ @Test(dataProvider = "jobStatuses")
+ public void jobStatusChanged_subJobStatusChangedCalledWithJobStatus(JobStatus status)
+ throws IOException
+ {
+ when(mockClient.getStatus(jobRef))
+ .thenReturn(status)
+ .thenReturn(JobStatus.COMPLETED);
+ var mockListener = performAction(viewport, actionBuilder.build());
+ verify(mockListener).subJobStatusChanged(any(), any(), eq(status));
+ }
+}
\ No newline at end of file
package jalview.ws2.client.slivka;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import org.hamcrest.Matcher;
import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import jalview.bin.Cache;
+import jalview.bin.Console;
+import jalview.ws.params.ValueConstrainI.ValueType;
+import jalview.ws.params.simple.DoubleParameter;
+import jalview.ws.params.simple.IntegerParameter;
+import jalview.ws.params.simple.StringParameter;
+import jalview.ws2.actions.alignment.AlignmentAction;
+import jalview.ws2.actions.annotation.AnnotationAction;
+import jalview.ws2.client.api.WebServiceDiscovererI;
+import uk.ac.dundee.compbio.slivkaclient.Parameter;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaClient;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaService;
+
public class SlivkaWSDiscovererTest
{
- @BeforeClass
- public void setupClass() throws IOException
+ private static final String URLS_PROPERTY_NAME = "SLIVKAHOSTURLS";
+
+ SlivkaClient clientMock;
+
+ Function<URL, SlivkaClient> factoryMock;
+
+ @BeforeClass(alwaysRun = true)
+ public void setupProperties()
+ {
+ Cache.loadProperties("test/jalview/ws2/client/slivka/default.jvprops");
+ Console.initLogger();
+ }
+
+ @BeforeMethod
+ public void setupDiscoverer() throws IOException
+ {
+ clientMock = mock(SlivkaClient.class);
+ }
+
+ @Test
+ public void getStatusForUrl_servicesReturned_statusIsOK() throws Exception
+ {
+ when(clientMock.getServices())
+ .thenReturn(List.of(mock(SlivkaService.class)));
+ var discoverer = new SlivkaWSDiscoverer(
+ url -> url.toString().equals("http://example.org") ? clientMock
+ : null);
+ assertThat(discoverer.getStatusForUrl(new URL("http://example.org")),
+ is(WebServiceDiscovererI.STATUS_OK));
+ }
+
+ @Test
+ public void getStatusForUrl_noServicesReturned_statusIsNoServices()
+ throws Exception
+ {
+ when(clientMock.getServices()).thenReturn(List.of());
+ var discoverer = new SlivkaWSDiscoverer(
+ url -> url.toString().equals("http://example.org") ? clientMock
+ : null);
+ assertThat(discoverer.getStatusForUrl(new URL("http://example.org")),
+ is(WebServiceDiscovererI.STATUS_NO_SERVICES));
+ }
+
+ @Test
+ public void getStatusForUrl_exceptionThrown_statusIsInvalid()
+ throws Exception
+ {
+ when(clientMock.getServices()).thenThrow(new IOException());
+ var discoverer = new SlivkaWSDiscoverer(
+ url -> url.toString().equals("http://example.org") ? clientMock
+ : null);
+ assertThat(discoverer.getStatusForUrl(new URL("http://example.org")),
+ is(WebServiceDiscovererI.STATUS_INVALID));
+ }
+
+ @Test
+ public void testGetUrls_noPropEntry_defaultUrlReturned()
+ throws MalformedURLException
{
var discoverer = SlivkaWSDiscoverer.getInstance();
-
+ assertThat(discoverer.getUrls(),
+ contains(new URL("https://www.compbio.dundee.ac.uk/slivka/")));
}
-
+
+ @DataProvider
+ public Object[][] urlPropertyValues() throws MalformedURLException
+ {
+ return new Object[][] {
+ { "http://example.org/", List.of(new URL("http://example.org/")) },
+ { "https://example.org/slivka/",
+ List.of(new URL("https://example.org/slivka/")) },
+ { "https://www.compbio.dundee.ac.uk/,http://www.example.org/",
+ List.of(new URL("https://www.compbio.dundee.ac.uk/"),
+ new URL("http://www.example.org/")) },
+ { "http://example.org/,", List.of(new URL("http://example.org/")) },
+ { ",http://example.org", List.of(new URL("http://example.org")) },
+ { "", List.of() },
+ { ",", List.of() },
+ { "example.org", List.of() },
+ { "example.org,http://example.org",
+ List.of(new URL("http://example.org")) } };
+ }
+
+ @Test(dataProvider = "urlPropertyValues")
+ public void testGetUrls_urlsProperlyParsed(String propValue,
+ List<URL> expected)
+ {
+ Cache.setProperty(URLS_PROPERTY_NAME, propValue);
+ var discoverer = SlivkaWSDiscoverer.getInstance();
+ assertThat(discoverer.getUrls(), equalTo(expected));
+ }
+
@Test
- public void testServiceFetch() throws IOException
+ public void testSetUrls_emptyList_propertyReset()
{
+ Cache.setProperty(URLS_PROPERTY_NAME, "http://www.example.org");
var discoverer = SlivkaWSDiscoverer.getInstance();
- var services = discoverer.fetchServices(discoverer.getDefaultUrl());
- for (var service : services)
+ discoverer.setUrls(List.of());
+ assertThat(Cache.getProperty(URLS_PROPERTY_NAME), is(nullValue()));
+ }
+
+ @Test
+ public void testSetUrls_null_propertyReset()
+ {
+ Cache.setProperty(URLS_PROPERTY_NAME, "http://www.example.org");
+ var discoverer = SlivkaWSDiscoverer.getInstance();
+ discoverer.setUrls(null);
+ assertThat(Cache.getProperty(URLS_PROPERTY_NAME), is(nullValue()));
+ }
+
+ @DataProvider
+ public Object[][] urlsList() throws MalformedURLException
+ {
+ return new Object[][] {
+ { List.of(new URL("http://example.org")), "http://example.org" },
+ { List.of(new URL("http://example.org/")), "http://example.org/" },
+ { List.of(new URL("http://example.org/slivka/")),
+ "http://example.org/slivka/" },
+ { List.of(new URL("https://www.compbio.dundee.ac.uk/slivka/"),
+ new URL("http://example.org")),
+ "https://www.compbio.dundee.ac.uk/slivka/,http://example.org" }, };
+ }
+
+ @Test(dataProvider = "urlsList")
+ public void testSetUrls_urlsPropertySet(List<URL> urls, String expected)
+ throws MalformedURLException
+ {
+ var discoverer = SlivkaWSDiscoverer.getInstance();
+ discoverer.setUrls(urls);
+ assertThat(Cache.getProperty(URLS_PROPERTY_NAME), equalTo(expected));
+ }
+
+ @Test
+ public void testFetchServices_oneService_basicDataMatches()
+ throws IOException
+ {
+ var service = new SlivkaService(
+ URI.create("http://example.org/api/services/example"),
+ "example", "Example name", "Example service description",
+ "John Smith", "1.0", "MIT License",
+ List.of("operation::analysis::multiple sequence alignment"),
+ List.of(), List.of(), null);
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ assertThat(webServices, hasSize(1));
+ var webService = webServices.get(0);
+ assertThat(webService.getUrl(),
+ equalTo(new URL("http://example.org/")));
+ assertThat(webService.getClientName(), equalTo("slivka"));
+ assertThat(webService.getName(), equalTo("Example name"));
+ assertThat(webService.getDescription(),
+ equalTo("Example service description"));
+ }
+
+ @DataProvider
+ public String[] validMultipleSequenceAlignmentClassifiers()
+ {
+ return new String[] {
+ "Operation :: Analysis :: Multiple sequence alignment",
+ "operation :: analysis :: multiple sequence alignment",
+ "Operation\t::\tAnalysis\t::\tMultiple sequence alignment",
+ "Operation::Analysis::Multiple sequence alignment",
+ "Operation :: Analysis :: Multiple Sequence Alignment",
+ "OPERATION :: ANALYSIS :: MULTIPLE SEQUENCE ALIGNMENT",
+ "Operation :: Analysis :: Sequence alignment :: Multiple sequence alignment",
+ "Operation :: Analysis :: Sequence analysis :: Sequence alignment :: Multiple sequence alignment",
+ "Operation :: Alignment :: Multiple sequence alignment",
+ "Operation :: Alignment :: Sequence alignment :: Multiple sequence alignment",
+ "Operation :: Comparison :: Multiple sequence alignment",
+ "Operation :: Comparison :: Sequence comparison :: Sequence alignment :: Multiple sequence alignment" };
+
+ }
+
+ @Test(dataProvider = "validMultipleSequenceAlignmentClassifiers")
+ public void testFetchServices_multipleSequenceAlignmentClassifier_serviceTypeIsMSA(
+ String classifier) throws IOException
+ {
+ var service = new SlivkaService(URI.create("http://example.org/"),
+ "example", "name", "description", "author", "1.0", "MIT",
+ List.of(classifier), List.of(), List.of(), null);
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ assertThat(webServices, hasSize(1));
+ assertThat(webServices.get(0).getCategory(), equalTo("Alignment"));
+ assertThat(webServices.get(0).getActionClass(),
+ typeCompatibleWith(AlignmentAction.class));
+ }
+
+ @DataProvider
+ public SlivkaService[] multipleSequenceAlignmentService()
+ {
+ return new SlivkaService[] {
+ new SlivkaService(
+ URI.create("http://example.org/"), "example", "Examaple name",
+ "Example description", "John Smith", "1.0", "MIT",
+ List.of("Operation :: Analysis :: Multiple sequence alignment"),
+ List.of(), List.of(), null),
+ new SlivkaService(
+ URI.create("http://example.org/api/services/muscle"),
+ "muscle", "MUSCLE",
+ "MUltiple Sequence Comparison by Log- Expectation",
+ "Robert C. Edgar", "3.8.31", "Public domain",
+ List.of("Topic :: Computational biology :: Sequence analysis",
+ "Operation :: Analysis :: Sequence analysis :: Sequence alignment :: Multiple sequence alignment"),
+ List.of(), List.of(), null),
+ new SlivkaService(
+ URI.create("http://example.org/api/services/tcoffee"),
+ "tcoffee", "TCoffee",
+ "Tree-based Consistency Objective Function for Alignment Evaluation",
+ "Cedric Notredame", "13.41.0", "GNU GPL",
+ List.of("Topic :: Computational biology :: Sequence analysis",
+ "Operation :: Analysis :: Sequence analysis :: Sequence alignment :: Multiple sequence alignment"),
+ List.of(), List.of(), null) };
+ }
+
+ @Test(dataProvider = "multipleSequenceAlignmentService")
+ public void testFetchServices_multipleSequenceAlignmentService_actionTypeIsAlignment(
+ SlivkaService service) throws IOException
+ {
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ assertThat(webServices.get(0).getCategory(), equalTo("Alignment"));
+ assertThat(webServices.get(0).getActionClass(),
+ typeCompatibleWith(AlignmentAction.class));
+ }
+
+ @Test(dataProvider = "multipleSequenceAlignmentService")
+ public void testFetchServices_multipleSequenceAlignmentService_serviceIsNonInteractive(
+ SlivkaService service) throws IOException
+ {
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ assertThat(webServices.get(0).isInteractive(), is(false));
+ }
+
+ @DataProvider
+ public SlivkaService[] clustalFamilyService()
+ {
+ return new SlivkaService[] {
+ new SlivkaService(
+ URI.create("http://example.org/api/services/clustalo"),
+ "clustalo", "ClustalO",
+ "Clustal Omega is the latest addition to the Clustal family.",
+ "Fabian Sievers, et al.", "1.2.4", "GNU GPL ver. 2",
+ List.of("Topic :: Computational biology :: Sequence analysis",
+ "Operation :: Analysis :: Sequence analysis :: Sequence alignment :: Multiple sequence alignment"),
+ List.of(), List.of(), null),
+ new SlivkaService(
+ URI.create("http://example.org/api/services/clustalw"),
+ "clustalw", "ClustalW",
+ "ClustalW is a general purpose multiple alignment program.",
+ "Larkin MA, et al.", "2.1", "GNU GPL ver. 3",
+ List.of("Topic :: Computation biology :: Sequence analysis",
+ "Operation :: Analysis :: Multiple sequence alignment"),
+ List.of(), List.of(), null),
+ new SlivkaService(
+ URI.create("http://example.org/api/services/clustalw2"),
+ "clustalw2", "ClustalW2",
+ "ClustalW is a general purpose multiple alignment program.",
+ "Larkin MA, et al.", "2.1", "GNU GPL ver. 3",
+ List.of("Topic :: Computation biology :: Sequence analysis",
+ "Operation :: Analysis :: Multiple sequence alignment"),
+ List.of(), List.of(), null), };
+ }
+
+ @Test(dataProvider = "clustalFamilyService")
+ public void testFetchService_clustalFamilyService_containsTwoActions(
+ SlivkaService service) throws IOException
+ {
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org"));
+ var actions = webServices.get(0).getActions();
+ assertThat(actions, hasSize(2));
+ assertThat(actions.get(0), allOf(hasProperty("name", is("Alignment")),
+ hasProperty("subcategory", is("Align"))));
+ assertThat(actions.get(1),
+ allOf(hasProperty("name", is("Re-alignment")),
+ hasProperty("subcategory", is("Realign"))));
+ }
+
+ @DataProvider
+ public String[] validRNASecondaryStructurePredictionClassifiers()
+ {
+ return new String[] {
+ "Operation :: Analysis :: RNA secondary structure prediction",
+ "operation :: analysis :: rna secondary structure prediction",
+ "OPERATION :: ANALYSIS :: RNA SECONDARY STRUCTURE PREDICTION",
+ "Operation\t::\tAnalysis\t::\tRNA secondary structure prediction",
+ "Operation::Analysis::RNA secondary structure prediction",
+ "Operation :: Analysis :: Structure analysis :: RNA secondary structure prediction",
+ "Operation :: Analysis :: Structure analysis :: Nucleic acid structure analysis :: RNA secondary structure analysis :: RNA secondary structure prediction",
+ "Operation :: Analysis :: Structure analysis :: Nucleic acid structure analysis :: Nucleic acid structure prediction :: RNA secondary structure prediction",
+ "Operation :: Analysis :: Sequence analysis :: Nucleic acid sequence analysis :: Nucleic acid feature detection :: RNA secondary structure prediction",
+ "Operation :: Prediction and recognition :: RNA secondary structure prediction",
+ "Operation :: Prediction and recognition :: Nucleic acid feature detection :: RNA secondary structure prediction",
+ "Operation :: Prediction and recignition :: Nucleic acid structure prediction :: RNA secondary structure prediction", };
+ }
+
+ @DataProvider
+ public Iterator<Object> RNASecondaryStructurePredictionService()
+ {
+ var services = new ArrayList<>();
+ for (var classifier : validRNASecondaryStructurePredictionClassifiers())
+ {
+ services.add(new SlivkaService(URI.create("http://example.org/"),
+ "example", "name", "description", "author", "1.0", "MIT",
+ List.of(classifier), List.of(), List.of(), null));
+ }
+ return services.iterator();
+ }
+
+ @Test(dataProvider = "RNASecondaryStructurePredictionService")
+ public void testFetchServices_RNASecStrPredClassifier_serviceTypeIsRNASecStrPred(
+ SlivkaService service) throws IOException
+ {
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ assertThat(webServices, hasSize(1));
+ assertThat(webServices.get(0).getCategory(),
+ equalTo("Secondary Structure Prediction"));
+ assertThat(webServices.get(0).getActionClass(),
+ typeCompatibleWith(AnnotationAction.class));
+ }
+
+ @DataProvider
+ public String[] validConservationAnalysisClassifiers()
+ {
+ return new String[] {
+ "Operation :: Analysis :: Sequence alignment analysis (conservation)",
+ "Operation::Analysis::Sequence alignment analysis (conservation)",
+ "Operation\t::\tAnalysis\t::\tSequence alignment analysis (conservation)",
+ "Operation :: Analysis :: Sequence analysis :: Sequence alignment analysis (conservation)",
+ "Operation :: Analysis :: Sequence analysis :: Sequence alignment analysis :: Sequence alignment analysis (conservation)", };
+ }
+
+ @DataProvider
+ public Iterator<Object> ConservationAnalysisService()
+ {
+ var services = new ArrayList<>();
+ for (var classifier : validConservationAnalysisClassifiers())
{
- System.out.format("Service(%s>%s @%s)%n", service.getCategory(),
- service.getName(), service.getUrl());
- var datastore = service.getParamDatastore();
- for (var param : datastore.getServiceParameters())
- {
- System.out.format(" %s :%s%n", param.getName(), param.getClass().getSimpleName());
- }
+ services.add(new SlivkaService(URI.create("http://example.org/"),
+ "example", "name", "description", "author", "1.0", "MIT",
+ List.of(classifier), List.of(), List.of(), null));
}
+ return services.iterator();
+ }
+
+ @Test(dataProvider = "validConservationAnalysisClassifiers")
+ public void testFetchServices_conservationAnalysisClassifier_serviceTypeIsConservation(
+ String classifier) throws IOException
+ {
+ var service = new SlivkaService(URI.create("http://example.org/"),
+ "example", "name", "description", "author", "1.0", "MIT",
+ List.of(classifier), List.of(), List.of(), null);
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ assertThat(webServices, hasSize(1));
+ assertThat(webServices.get(0).getCategory(), equalTo("Conservation"));
+ assertThat(webServices.get(0).getActionClass(),
+ typeCompatibleWith(AnnotationAction.class));
+ }
+
+ @DataProvider
+ public Object[] validProteinSequenceAnalysisClassifiers()
+ {
+ return new Object[] {
+ "Operation :: Analysis :: Sequence analysis :: Protein sequence analysis", };
+ }
+
+ @Test(dataProvider = "validProteinSequenceAnalysisClassifiers")
+ public void testFetchServices_proteinSequenceAnalysisClassifier_serviceTypeIsProtSeqAnalysis(
+ String classifier) throws IOException
+ {
+ var service = new SlivkaService(URI.create("http://example.org/"),
+ "example", "name", "description", "author", "1.0", "MIT",
+ List.of(classifier), List.of(), List.of(), null);
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ assertThat(webServices, hasSize(1));
+ assertThat(webServices.get(0).getCategory(),
+ equalTo("Protein Disorder"));
+ assertThat(webServices.get(0).getActionClass(),
+ typeCompatibleWith(AnnotationAction.class));
+ }
+
+ @DataProvider
+ public Object[] validProteinSecondaryStructurePredictionClassifiers()
+ {
+ return new Object[] {
+ "Operation ;: Analysis :: Protein secondary structure prediction",
+ "Operation :: Analysis :: Structure analysis :: Protein structure analysis :: Protein secondary structure analysis :: Protein secondary structure prediction",
+ "Operation :: Analysis :: Sequence analysis :: Protein sequence analysis :: Protein feature detection :: Protein secondary structure prediction",
+ "Operation :: Analysis :: Sequence analysis :: Protein sequence analysis :: Protein secondary structure prediction",
+ "Operation :: Prediction and recognition :: Protein secondary structure prediction",
+ "Operation :: Prediction and recognition :: Protein feature detection :: Protein secondary structure prediction", };
+ }
+
+ @Test(
+ enabled = false, // sec. str. pred. not implemented for slivka
+ dataProvider = "validProteinSecondaryStructurePredictionClassifiers")
+ public void testFetchServices_proteinSecStrPredClassifier_serviceTypeIsProtSecStrPred(
+ String classifier) throws IOException
+ {
+ var service = new SlivkaService(URI.create("http://example.org/"),
+ "example", "name", "description", "author", "1.0", "MIT",
+ List.of(classifier), List.of(), List.of(), null);
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ assertThat(webServices, hasSize(1));
+ assertThat(webServices.get(0).getCategory(),
+ equalTo("Protein Disorder"));
+ assertThat(webServices.get(0).getActionClass(),
+ typeCompatibleWith(AnnotationAction.class));
+ }
+
+ @DataProvider
+ public SlivkaService[] unrecognisedService()
+ {
+ return new SlivkaService[] {
+ new SlivkaService(URI.create("http://example.org/"), "example",
+ "Example name", "Example description", "John Smith",
+ "1.0.0", "Apache License, version 2.0",
+ List.of("This :: Classifier :: Does not exist"), List.of(),
+ List.of(), null) };
+ }
+
+ @Test(dataProvider = "unrecognisedService")
+ public void testFetchServices_unrecognisedService_noServiceDiscovered(
+ SlivkaService service) throws IOException
+ {
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org"));
+ assertThat(webServices, hasSize(0));
+ }
+
+ @DataProvider
+ public Object[] serviceParameterAndMappedClass()
+ {
+ return new Object[][] {
+ {
+ new Parameter.IntegerParameter("param", "Parameter", "Description",
+ true, false, null, Map.of(), null, null),
+ IntegerParameter.class
+ },
+ {
+ new Parameter.DecimalParameter("param", "Parameter",
+ "Description", true, false, null, Map.of(), null, null,
+ false, false),
+ DoubleParameter.class
+ },
+ {
+ new Parameter.TextParameter("param", "Parameter", "Description",
+ true, false, null, Map.of(), 0, null),
+ StringParameter.class
+ },
+ {
+ new Parameter.FlagParameter("param", "Parameter", "Description",
+ true, false, null, Map.of()),
+ StringParameter.class
+ },
+ {
+ new Parameter.ChoiceParameter("param", "Parameter", "Description",
+ true, false, null, Map.of(), List.of()),
+ StringParameter.class
+ },
+ };
+ }
+
+ @Test(dataProvider = "serviceParameterAndMappedClass")
+ public void testServiceParameter_slivkaParameterMappedToJalviewParameter(
+ Parameter slivkaParameter, Class<?> expectedClass)
+ throws IOException
+ {
+ var service = new SlivkaService(URI.create("http://example.org"),
+ "example", "name", "description", "author", "1.0",
+ "MIT License",
+ List.of("Operation :: Analysis :: Multiple sequence alignment"),
+ List.of(slivkaParameter), List.of(), null);
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org"));
+ var paramDatastore = webServices.get(0).getParamDatastore();
+ var arguments = paramDatastore.getServiceParameters();
+ assertThat(arguments.get(0), instanceOf(expectedClass));
+ }
+
+ @DataProvider
+ public Object[][] serviceParametersAndPropertyMatcher()
+ {
+ return new Object[][] {
+ {
+ new Parameter.IntegerParameter("param1", "Parameter 1",
+ "Description of parameter 1", true, false, null, Map.of(),
+ null, null),
+ allOf(
+ hasProperty("name", equalTo("param1")),
+ hasProperty("label", equalTo("Parameter 1")),
+ hasProperty("description", equalTo("Description of parameter 1")),
+ hasProperty("required", is(true)),
+ hasProperty("value", nullValue()))
+ },
+ {
+ new Parameter.IntegerParameter("param2", null, null, true, false,
+ null, Map.of(), null, null),
+ allOf(
+ hasProperty("name", equalTo("param2")),
+ hasProperty("label", equalTo("param2")),
+ hasProperty("description", nullValue()),
+ hasProperty("required", is(true)),
+ hasProperty("value", nullValue()))
+ },
+ {
+ new Parameter.IntegerParameter("param3", "Parameter 3", "", false,
+ false, 12, Map.of(), null, null),
+ allOf(
+ hasProperty("name", equalTo("param3")),
+ hasProperty("label", equalTo("Parameter 3")),
+ hasProperty("description", equalTo("")),
+ hasProperty("required", is(false)),
+ hasProperty("value", equalTo("12")))
+ },
+ };
+ }
+
+ @Test(dataProvider = "serviceParametersAndInfoMatcher")
+ public void testServiceParameters_testBasicParameterProperties(
+ Parameter parameter, Matcher<Object> matcher) throws IOException
+ {
+ var service = new SlivkaService(URI.create("http://example.org/"),
+ "example", "Example name", "Example description", "John Smith",
+ "1.0", "MIT",
+ List.of("Operation :: Analysis :: Multiple sequence alignment"),
+ List.of(parameter), List.of(), null);
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ var paramDatastore = webServices.get(0).getParamDatastore();
+ var arguments = paramDatastore.getServiceParameters();
+ assertThat(arguments.get(0), matcher);
+ }
+
+ @DataProvider
+ public Object[][] integerParametersAndPropertyMatcher()
+ {
+ return new Object[][] {
+ {
+ new Parameter.IntegerParameter("param", null, null, true, false,
+ null, Map.of(), null, null),
+ hasProperty("validValue", hasProperty("type", is(ValueType.Integer)))
+ },
+ {
+ new Parameter.IntegerParameter("param", null, null, true, false,
+ null, Map.of(), null, null),
+ hasProperty("validValue", allOf(
+ hasProperty("min", nullValue()),
+ hasProperty("max", nullValue()))),
+ },
+ {
+ new Parameter.IntegerParameter("param", null, null, true, false,
+ null, Map.of(), -12, 42),
+ hasProperty("validValue", allOf(
+ hasProperty("min", is(-12)),
+ hasProperty("max", is(42))))
+ },
+ };
+ }
+
+ @Test(dataProvider = "integerParametersAndPropertyMatcher")
+ public void testServiceParameters_testIntegerProperties(
+ Parameter parameter, Matcher<Object> matcher) throws IOException
+ {
+ var service = new SlivkaService(URI.create("http://example.org"),
+ "example", "Example name", "Example description", "John Smith",
+ "1.0", "MIT",
+ List.of("Operation :: Analysis :: Multiple Sequence Alignment"),
+ List.of(parameter), List.of(), null);
+ when(clientMock.getServices()).thenReturn(List.of(service));
+ when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/"));
+ var discoverer = new SlivkaWSDiscoverer(url -> clientMock);
+ var webServices = discoverer
+ .fetchServices(new URL("http://example.org/"));
+ var paramDatastore = webServices.get(0).getParamDatastore();
+ var arguments = paramDatastore.getServiceParameters();
+ assertThat(arguments.get(0), matcher);
}
}
--- /dev/null
+#---JalviewX Properties File---
+#Wed Jun 07 18:01:12 CET 2023