--- /dev/null
+package jalview.ws2.gui;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
+import jalview.util.MessageManager;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.PollingTaskExecutor;
+import jalview.ws2.operations.PhmmerOperation;
+import jalview.ws2.operations.PhmmerWorker;
+
+public class PhmmerMenuBuilder implements MenuEntryProviderI
+{
+ PhmmerOperation operation;
+
+ public PhmmerMenuBuilder(PhmmerOperation operation)
+ {
+ this.operation = operation;
+ }
+
+ @Override
+ public void buildMenu(JMenu parent, AlignFrame frame)
+ {
+ {
+ var item = new JMenuItem(MessageManager.formatMessage(
+ "label.calcname_with_default_settings", operation.getName()));
+ item.addActionListener((event) -> {
+ // TODO: not sure if the right way to get sequences for phmmer
+ final AlignmentView msa = frame.gatherSequencesForAlignment();
+ if (msa != null)
+ {
+ startWorker(frame, msa, Collections.emptyList());
+ }
+ });
+ parent.add(item);
+ }
+ if (operation.hasParameters())
+ {
+ var item = new JMenuItem(MessageManager.getString("label.edit_settings_and_run"));
+ item.setToolTipText(MessageManager.getString("label.view_and_change_parameters_before_running_calculation"));
+ item.addActionListener((event) -> {
+ final AlignmentView msa = frame.gatherSequencesForAlignment();
+ if (msa != null)
+ {
+ MenuEntryProviderI.openEditParamsDialog(operation.getParamStore(),
+ null, null)
+ .thenAcceptAsync((arguments) -> {
+ if (arguments != null)
+ {
+ startWorker(frame, msa, arguments);
+ }
+ });
+ }
+ });
+ }
+ SequenceI[] sequences = frame.getViewport().getAlignment().getSequencesArray();
+ }
+
+ private void startWorker(AlignFrame frame, AlignmentView msa,
+ List<ArgumentI> arguments)
+ {
+ final AlignViewport viewport = frame.getViewport();
+ final PollingTaskExecutor executor = viewport.getWSExecutor();
+ if (msa != null)
+ {
+ PhmmerWorker worker = new PhmmerWorker(operation, msa, arguments);
+ executor.submit(worker);
+ }
+ }
+}
--- /dev/null
+package jalview.ws2.hmmer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jalview.hmmer.rest.PhmmerClient;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.WsParamSetI;
+import jalview.ws.params.simple.*;
+
+class ParamSetFactory
+{
+ private static class SimpleParamSet implements WsParamSetI
+ {
+ private String name;
+ private String description;
+ private String[] applicableUrls;
+ private String sourceFile = null;
+ private List<ArgumentI> arguments;
+
+
+ @Override
+ public String getName()
+ {
+ return name;
+ }
+
+ @Override
+ public String getDescription()
+ {
+ return description;
+ }
+
+ @Override
+ public String[] getApplicableUrls()
+ {
+ return applicableUrls;
+ }
+
+ @Override
+ public String getSourceFile()
+ {
+ return sourceFile;
+ }
+
+ @Override
+ public void setSourceFile(String newfile)
+ {
+ this.sourceFile = newfile;
+ }
+
+ @Override
+ public boolean isModifiable()
+ {
+ return true;
+ }
+
+ @Override
+ public List<ArgumentI> getArguments()
+ {
+ return arguments;
+ }
+
+ @Override
+ public void setArguments(List<ArgumentI> args)
+ {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public static WsParamSetI newDefaultParamSet(PhmmerClient client)
+ {
+ SimpleParamSet paramSet = new SimpleParamSet();
+ paramSet.name = "Default";
+ paramSet.description = "Default set of parameters";
+ paramSet.applicableUrls = new String[]{ PhmmerClient.getDefaultURL() };
+ List<ArgumentI> arguments = new ArrayList<>();
+ var reqBuilder = client.newRequestBuilder();
+
+ arguments.add(new StringParameter(
+ "Sequence Database", "Sequence Database Selection.", true,
+ reqBuilder.getDefaultDatabase(), reqBuilder.getDefaultDatabase(),
+ reqBuilder.getAllowedDatabaseValues(),
+ reqBuilder.getAllowedDatabaseValues()));
+ arguments.add(new BooleanOption("Output Alignment", "Output alignment in result.",
+ false, reqBuilder.getDefaultAlignView(), reqBuilder.getDefaultAlignView(), null));
+ arguments.add(new IntegerParameter(
+ "Number of Hits Displayed", "Number of hits to be displayed.", false,
+ reqBuilder.getDefaultNhits(), 0, 100000));
+
+ arguments.add(new RadioChoiceParameter(
+ "Cut-offs", "Cut-off type to be used.", List.of("E-value", "Bit score"),
+ "E-value"));
+
+ arguments.add(new LogarithmicParameter(
+ "Significance E-values[Sequence]", "Significance E-values[Sequence]",
+ false, (double) reqBuilder.getDefaultIncE(), Double.MIN_VALUE, 10.0));
+ arguments.add(new LogarithmicParameter(
+ "Significance E-values[Hit]", "Significance E-values[Hit]", false,
+ (double) reqBuilder.getDefaultIncdomE(), Double.MIN_VALUE, 10.0));
+ arguments.add(new LogarithmicParameter(
+ "Report E-values[Sequence]", "Report E-values[Sequence]", false,
+ (double) reqBuilder.getDefaultE(), Double.MIN_VALUE, 10.0));
+ arguments.add(new LogarithmicParameter(
+ "Report E-values[Hit]", "Report E-values[Hit]", false,
+ (double) reqBuilder.getDefaultDomE(), Double.MIN_VALUE, 10.0));
+
+ arguments.add(new DoubleParameter(
+ "Significance bit scores[Sequence]", "Significance bit scores[Sequence]",
+ false, (double) reqBuilder.getDefaultIncT(), 0.0, 10000.0));
+ arguments.add(new DoubleParameter(
+ "Significance bit scores[Hit]", "Significance bit scores[Hit]",
+ false, (double) reqBuilder.getDefaultIncdomT(), 0.0, 10000.0));
+ arguments.add(new DoubleParameter(
+ "Report bit scores[Sequence]", "Report bit scores[Sequence]",
+ false, (double) reqBuilder.getDefaultT(), 0.0, 10000.0));
+ arguments.add(new DoubleParameter(
+ "Report bit scores[Hit]", "Report bit scores[Hit]", false,
+ (double) reqBuilder.getDefaultDomT(), 0.0, 10000.0));
+
+ arguments.add(new DoubleParameter(
+ "Gap Penalties[open]", "Gap Penalties[open]", false,
+ (double) reqBuilder.getDefaultPopen(), 0.0, 0.5));
+ arguments.add(new DoubleParameter(
+ "Gap Penalties[extend]", "Gap Penalties[extend]", false,
+ (double) reqBuilder.getDefaultPextend(), 0.0, 1.0));
+ arguments.add(new StringParameter(
+ "Gap Penalties[Substitution scoring matrix]",
+ "Gap Penalties[Substitution scoring matrix]",
+ false, reqBuilder.getDefaultMx(), reqBuilder.getDefaultMx(),
+ reqBuilder.getAllowedMxValues(), reqBuilder.getAllowedMxValues()));
+ arguments.add(new BooleanOption(
+ "No bias filter", "Disable bias composition filter which is on by default",
+ false, reqBuilder.getDefaultNoBias(), reqBuilder.getDefaultNoBias(), null));
+
+ paramSet.arguments = arguments;
+ return paramSet;
+ }
+}
--- /dev/null
+package jalview.ws2.hmmer;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import jalview.hmmer.rest.PhmmerClient;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.WsParamSetI;
+
+public class PhmmerParamDatastore implements ParamDatastoreI
+{
+ private WsParamSetI defaultPreset;
+ private List<WsParamSetI> presets = List.of();
+
+ public PhmmerParamDatastore(PhmmerClient client) {
+ defaultPreset = ParamSetFactory.newDefaultParamSet(client);
+ }
+
+ @Override
+ public List<WsParamSetI> getPresets()
+ {
+ return presets;
+ }
+
+ @Override
+ public WsParamSetI getPreset(String name)
+ {
+ for (WsParamSetI preset : presets)
+ if (preset.getName().equals(name))
+ return preset;
+ return null;
+ }
+
+ @Override
+ public List<ArgumentI> getServiceParameters()
+ {
+ return Collections.unmodifiableList(defaultPreset.getArguments());
+ }
+
+ @Override
+ public boolean presetExists(String name)
+ {
+ for (WsParamSetI preset : presets)
+ if (preset.getName().equals(name))
+ return true;
+ return false;
+ }
+
+ @Override
+ public void deletePreset(String name)
+ {
+ }
+
+ @Override
+ public void storePreset(String presetName, String text, List<ArgumentI> jobParams)
+ {
+ }
+
+ @Override
+ public void updatePreset(String oldName, String presetName, String text, List<ArgumentI> jobParams)
+ {
+ }
+
+ @Override
+ public WsParamSetI parseServiceParameterFile(String name, String description, String[] serviceURL, String parameters)
+ throws IOException
+ {
+ return null;
+ }
+
+ @Override
+ public String generateServiceParameterFile(WsParamSetI pset) throws IOException
+ {
+ return null;
+ }
+
+}
--- /dev/null
+package jalview.ws2.hmmer;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jalview.bin.Cache;
+import jalview.hmmer.rest.PhmmerClient;
+import jalview.ws2.OperationsChangeListenerList;
+import jalview.ws2.WebServiceDiscovererI;
+import jalview.ws2.operations.Operation;
+import jalview.ws2.operations.PhmmerOperation;
+
+import static java.lang.String.format;
+
+
+public class PhmmerWSDiscoverer implements WebServiceDiscovererI
+{
+ private static final String DEFAULT_PHMMER_URL = PhmmerClient.getDefaultURL();
+
+ private static PhmmerWSDiscoverer instance = null;
+
+ private List<Operation> operations = List.of();
+
+ private PhmmerWSDiscoverer()
+ {
+ }
+
+ public static PhmmerWSDiscoverer getInstance()
+ {
+ if (instance == null)
+ instance = new PhmmerWSDiscoverer();
+ return instance;
+ }
+
+ @Override
+ public List<String> getUrls()
+ {
+ return List.of(DEFAULT_PHMMER_URL);
+ }
+
+ @Override
+ public void setUrls(List<String> wsUrls)
+ {
+ throw new UnsupportedOperationException("setting url is not supported");
+ }
+
+ @Override
+ public boolean testUrl(URL url)
+ {
+ return getStatusForUrl(url.toString()) == STATUS_OK;
+ }
+
+ @Override
+ public int getStatusForUrl(String url)
+ {
+ try {
+ PhmmerClient.create(url);
+ return STATUS_OK;
+ }
+ catch (IOException e) {
+ return STATUS_INVALID;
+ }
+ }
+
+ @Override
+ public List<Operation> getOperations()
+ {
+ return operations;
+ }
+
+ @Override
+ public boolean hasServices()
+ {
+ return !isRunning() && operations.size() > 0;
+ }
+
+ private static final int END = 0x01;
+ private static final int BEGIN = 0x02;
+ private static final int AGAIN = 0x04;
+ private final AtomicInteger state = new AtomicInteger(END);
+ private CompletableFuture<WebServiceDiscovererI> discoveryTask = new CompletableFuture<>();
+
+ @Override
+ public boolean isRunning()
+ {
+ return (state.get() & (BEGIN | AGAIN)) != 0;
+ }
+
+ @Override
+ public boolean isDone()
+ {
+ return state.get() == END && discoveryTask != null && discoveryTask.isDone();
+ }
+
+ @Override
+ public synchronized CompletableFuture<WebServiceDiscovererI> startDiscoverer()
+ {
+ while (true)
+ {
+ if (state.get() == AGAIN)
+ {
+ break;
+ }
+ else if (state.compareAndSet(END, BEGIN) || state.compareAndSet(BEGIN, AGAIN))
+ {
+ final var oldTask = discoveryTask;
+ CompletableFuture<WebServiceDiscovererI> task = oldTask
+ .handleAsync((r, e) -> {
+ reloadServices();
+ return PhmmerWSDiscoverer.this;
+ });
+ task.thenRun(() -> {
+ while (true)
+ {
+ if (state.compareAndSet(BEGIN, END) || state.compareAndSet(AGAIN, BEGIN))
+ {
+ break;
+ }
+ }
+ fireOperationsChanged(getOperations());
+ });
+ oldTask.cancel(false);
+ discoveryTask = task;
+ break;
+ }
+ }
+ return discoveryTask;
+ }
+
+ private List<Operation> reloadServices()
+ {
+ fireOperationsChanged(Collections.emptyList());
+ ArrayList<Operation> allOperations = new ArrayList<>();
+ for (String url : getUrls())
+ {
+ PhmmerClient client;
+ try
+ {
+ client = PhmmerClient.create(url);
+ } catch (IOException e)
+ {
+ Cache.log.error(format("Unable to create phmmer client for url %s", url), e);
+ continue;
+ }
+ PhmmerWebService webService = new PhmmerWebService(client);
+ PhmmerOperation operation = new PhmmerOperation(webService, webService::getPhmmerResult);
+ allOperations.add(operation);
+ }
+ this.operations = Collections.unmodifiableList(allOperations);
+ Cache.log.info("Reloading phmmer services finished");
+ return this.operations;
+ }
+
+ @Override
+ public String getErrorMessages()
+ {
+ return "";
+ }
+
+ private OperationsChangeListenerList operationsChangeListeners =
+ new OperationsChangeListenerList(this);
+
+ @Override
+ public void addOperationsChangeListener(OperationsChangeListener listener)
+ {
+ operationsChangeListeners.addListener(listener);
+ }
+
+ @Override
+ public void removeOperationsChangeListener(OperationsChangeListener listener)
+ {
+ operationsChangeListeners.removeListener(listener);
+ }
+
+ private void fireOperationsChanged(List<Operation> list)
+ {
+ operationsChangeListeners.fireOperationsChanged(list);
+ }
+
+}
--- /dev/null
+package jalview.ws2.hmmer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import jalview.bin.Cache;
+import jalview.datamodel.SequenceI;
+import jalview.hmmer.rest.PhmmerClient;
+import jalview.hmmer.rest.PhmmerRequest;
+import jalview.hmmer.rest.PhmmerRequestBuilder;
+import jalview.io.FileFormat;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws2.WSJob;
+import jalview.ws2.WSJobStatus;
+import jalview.ws2.WebServiceI;
+
+import static java.lang.String.format;
+
+public class PhmmerWebService implements WebServiceI
+{
+ protected final PhmmerClient client;
+ protected ParamDatastoreI store;
+
+ public PhmmerWebService(PhmmerClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public String getHostName()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getProviderName()
+ {
+ return "EBI HMMER";
+ }
+
+ @Override
+ public String getName()
+ {
+ return "phmmer";
+ }
+
+ @Override
+ public String getDescription()
+ {
+ return "HMMER phmmer searches a database of protein sequence with "
+ + "a protein sequence (protein sequence vs protein sequence database).";
+ }
+
+ @Override
+ public boolean hasParameters()
+ {
+ return getParamStore().getServiceParameters().size() > 0;
+ }
+
+ @Override
+ public ParamDatastoreI getParamStore()
+ {
+ if (store == null)
+ store = new PhmmerParamDatastore(client);
+ return store;
+ }
+
+ @Override
+ public String submit(List<SequenceI> sequences, List<ArgumentI> args) throws IOException
+ {
+ PhmmerRequestBuilder reqBuilder = client.newRequestBuilder();
+ String sequence = FileFormat.Fasta.getWriter(null)
+ .print(new SequenceI[]{ sequences.get(0) }, false);
+ reqBuilder.sequenceString(sequence);
+ boolean useBitScore = false;
+ for (ArgumentI arg : args) {
+ String value = arg.getValue();
+ if (value == null)
+ continue;
+ switch(arg.getName()) {
+ case "Sequence Database":
+ reqBuilder.database(value);
+ break;
+ case "Output Alignment":
+ reqBuilder.alignView(!value.isBlank() ? true : false);
+ break;
+ case "Number of Hits Displayed":
+ reqBuilder.nhits(Integer.parseInt(value));
+ break;
+ case "Cut-offs":
+ if (value.equals("E-value"))
+ useBitScore = false;
+ else if (value.equals("Bit score"))
+ useBitScore = true;
+ else
+ throw new IllegalArgumentException(format(
+ "Illegal cut off value \"%s\".", value));
+ break;
+ case "Gap Penalties[open]":
+ reqBuilder.popen(Float.parseFloat(value));
+ break;
+ case "Gap Penalties[extend]":
+ reqBuilder.popen(Float.parseFloat(value));
+ break;
+ case "Gap Penalties[Substitution scoring matrix]":
+ reqBuilder.mx(value);
+ break;
+ case "No bias filter":
+ reqBuilder.noBias(!value.isBlank() ? true : false);
+ break;
+ }
+ }
+ for (ArgumentI arg : args) {
+ String value = arg.getValue();
+ if (value == null)
+ continue;
+ switch (arg.getName()) {
+ case "Significance E-values[Sequence]":
+ if (useBitScore) break;
+ reqBuilder.incE(Float.parseFloat(value));
+ break;
+ case "Significance E-values[Hit]":
+ if (useBitScore) break;
+ reqBuilder.incdomE(Float.parseFloat(value));
+ break;
+ case "Report E-values[Sequence]":
+ if (useBitScore) break;
+ reqBuilder.E(Float.parseFloat(value));
+ break;
+ case "Report E-values[Hit]":
+ if (useBitScore) break;
+ reqBuilder.domE(Float.parseFloat(value));
+ break;
+ case "Significance bit scores[Sequence]":
+ if (!useBitScore) break;
+ reqBuilder.incT(Float.parseFloat(value));
+ break;
+ case "Significance bit scores[Hit]":
+ if (!useBitScore) break;
+ reqBuilder.incdomT(Float.parseFloat(value));
+ break;
+ case "Report bit scores[Sequence]":
+ if (!useBitScore) break;
+ reqBuilder.T(Float.parseFloat(value));
+ break;
+ case "Report bit scores[Hit]":
+ if (!useBitScore) break;
+ reqBuilder.domT(Float.parseFloat(value));
+ break;
+ }
+ }
+ PhmmerRequest request = reqBuilder.build();
+ return client.submitRequest(request, "user@example.org");
+ }
+
+ @Override
+ public void updateProgress(WSJob job) throws IOException
+ {
+ var status = client.pollStatus(job.getJobId());
+ switch(status) {
+ case PENDING:
+ job.setStatus(WSJobStatus.QUEUED); break;
+ case RUNNING:
+ job.setStatus(WSJobStatus.RUNNING); break;
+ case FINISHED:
+ job.setStatus(WSJobStatus.FINISHED); break;
+ case FAILURE:
+ job.setStatus(WSJobStatus.FAILED); break;
+ case NOT_FOUND:
+ job.setStatus(WSJobStatus.UNKNOWN); break;
+ case UNDEFINED:
+ job.setStatus(WSJobStatus.UNKNOWN); break;
+ }
+ }
+
+ @Override
+ public void cancel(WSJob job) throws IOException
+ {
+ job.setStatus(WSJobStatus.CANCELLED);
+ Cache.log.warn("phmmer does not implement job cancellation.");
+ }
+
+ @Override
+ public boolean handleSubmissionError(WSJob job, Exception ex)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean handleCollectionError(WSJob job, Exception ex)
+ {
+ return false;
+ }
+
+ public Object getPhmmerResult(WSJob job) throws IOException
+ {
+ InputStream is = client.getFileStream(job.getJobId(), "out");
+ return new String(is.readAllBytes(), "UTF-8");
+ }
+
+}
--- /dev/null
+package jalview.ws2.operations;
+
+import java.io.IOException;
+
+import jalview.ws2.WSJob;
+import jalview.ws2.WebServiceI;
+import jalview.ws2.gui.PhmmerMenuBuilder;
+import jalview.ws2.gui.MenuEntryProviderI;
+
+public class PhmmerOperation extends AbstractOperation
+{
+ /**
+ * Interface that the web service client must implement in order to
+ * support phmmer operations.
+ *
+ * TODO: Change the name of the interface and its method.
+ * TODO: Change the returned type.
+ */
+ public static interface PhmmerResultSupplier {
+ public Object getPhmmerResult(WSJob job) throws IOException;
+ }
+
+ private PhmmerResultSupplier resultSupplier;
+
+ public PhmmerOperation(WebServiceI service, PhmmerResultSupplier resultSupplier)
+ {
+ super(service, "HMMER");
+ this.resultSupplier = resultSupplier;
+ }
+
+ @Override
+ public MenuEntryProviderI getMenuBuilder()
+ {
+ return new PhmmerMenuBuilder(this);
+ }
+
+ public PhmmerResultSupplier getResultSupplier()
+ {
+ return resultSupplier;
+ }
+
+}
--- /dev/null
+package jalview.ws2.operations;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import java.util.List;
+
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignViewport;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.WSJob;
+import jalview.ws2.WebServiceI;
+
+
+public class PhmmerWorker extends AbstractPollableWorker
+{
+
+ // TODO store additional job information in the job object
+ private static class PhmmerJob extends WSJob
+ {
+ private PhmmerJob(String serviceProvider, String serviceName, String hostName)
+ {
+ super(serviceProvider, serviceName, hostName);
+ }
+ }
+
+ private final PhmmerOperation operation;
+ private final AlignmentView msa;
+ private final List<ArgumentI> args;
+ private WSJobList<PhmmerJob> jobs = new WSJobList<>();
+ private PhmmerJob job = null;
+
+ // TODO add constructor arguments which are necessary to run the job.
+ public PhmmerWorker(PhmmerOperation operation, AlignmentView msa, List<ArgumentI> args)
+ {
+ this.operation = operation;
+ this.msa = msa;
+ this.args = args;
+ }
+
+ @Override
+ public void start() throws IOException
+ {
+ Cache.log.info(format("Starting new %s job", operation.getName()));
+ /* TODO prepare input data and submit the job
+ * this is currently a placeholder which fetches all sequences
+ * the client submits only the first sequence */
+ List<SequenceI> sequences = msa.getVisibleAlignment('-').getSequences();
+ WebServiceI client = operation.getWebService();
+ job = new PhmmerJob(client.getProviderName(), client.getName(), client.getHostName());
+ jobs.add(job);
+ listeners.fireJobCreated(job);
+ String jobId = client.submit(sequences, args);
+ job.setJobId(jobId);
+ Cache.log.debug(format("Service %s: submitted job id %s", operation.getHostName(), jobId));
+ listeners.fireWorkerStarted();
+ }
+
+ @Override
+ public void done()
+ {
+ /* TODO when the job is finished, get the result from the supplier
+ * and do whatever is needed to display the result. For now, the result
+ * is printed to the logger */
+ Object result;
+ try
+ {
+ result = operation.getResultSupplier().getPhmmerResult(job);
+ }
+ catch (IOException e) {
+ Cache.log.debug(format("Failed to get results for job %s.", job.toString()));
+ return;
+ }
+ Cache.log.debug(result.toString());
+ }
+
+ @Override
+ public Operation getOperation()
+ {
+ return operation;
+ }
+
+ @Override
+ public WSJobList<? extends WSJob> getJobs()
+ {
+ return jobs;
+ }
+
+}