JAL-3954 Introduce hmmer rest client to jalview.
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Tue, 8 Feb 2022 19:57:41 +0000 (20:57 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Tue, 8 Feb 2022 19:57:41 +0000 (20:57 +0100)
src/jalview/hmmer/rest/FormURLEncodedBodyBuilder.java [new file with mode: 0644]
src/jalview/hmmer/rest/InputSequence.java [new file with mode: 0644]
src/jalview/hmmer/rest/PhmmerClient.java [new file with mode: 0644]
src/jalview/hmmer/rest/PhmmerRequest.java [new file with mode: 0644]
src/jalview/hmmer/rest/PhmmerRequestBuilder.java [new file with mode: 0644]
src/jalview/hmmer/rest/ResultType.java [new file with mode: 0644]
src/jalview/hmmer/rest/Status.java [new file with mode: 0644]

diff --git a/src/jalview/hmmer/rest/FormURLEncodedBodyBuilder.java b/src/jalview/hmmer/rest/FormURLEncodedBodyBuilder.java
new file mode 100644 (file)
index 0000000..04359a1
--- /dev/null
@@ -0,0 +1,58 @@
+package jalview.hmmer.rest;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public class FormURLEncodedBodyBuilder {
+  List<Map.Entry<String, String>> entries = new ArrayList<>();
+
+  public void addParameter(String parameter, String value) {
+    entries.add(new SimpleEntry<String, String>(parameter, value));
+  }
+
+  public byte[] build() {
+    StringBuilder builder = new StringBuilder();
+    var iter = entries.iterator();
+    Map.Entry<String, String> entry;
+    for (; iter.hasNext();) {
+      entry = iter.next();
+      builder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8))
+        .append('=')
+        .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
+      if (iter.hasNext())
+        builder.append('&');
+    }
+    return builder.toString().getBytes(StandardCharsets.UTF_8);
+  }
+
+  public void populateFromRequest(PhmmerRequest request) throws IOException {
+    addParameter("sequence", request.getSequence().getAsString());
+    addParameter("database", request.getDatabase());
+    addOptionalParameter("incE", request.getIncE());
+    addOptionalParameter("incdomE", request.getIncdomE());
+    addOptionalParameter("E", request.getE());
+    addOptionalParameter("domE", request.getDomE());
+    addOptionalParameter("incT", request.getIncT());
+    addOptionalParameter("incdomT", request.getIncdomT());
+    addOptionalParameter("T", request.getT());
+    addOptionalParameter("domT", request.getDomT());
+    addOptionalParameter("popen", request.getPopen());
+    addOptionalParameter("pextend", request.getPextend());
+    addOptionalParameter("mx", request.getMx());
+    addOptionalParameter("nobias", request.getNoBias());
+    addOptionalParameter("compressedout", request.getCompressedOut());
+    addOptionalParameter("alignView", request.getAlignView());
+    addOptionalParameter("evalue", request.getEvalue());
+    addOptionalParameter("nhits", request.getNhits());
+  }
+
+  private void addOptionalParameter(String param, Optional<?> value) {
+    value.ifPresent(val -> addParameter(param, val.toString()));
+  }
+}
diff --git a/src/jalview/hmmer/rest/InputSequence.java b/src/jalview/hmmer/rest/InputSequence.java
new file mode 100644 (file)
index 0000000..3735760
--- /dev/null
@@ -0,0 +1,7 @@
+package jalview.hmmer.rest;
+
+import java.io.IOException;
+
+public interface InputSequence {
+  String getAsString() throws IOException;
+}
diff --git a/src/jalview/hmmer/rest/PhmmerClient.java b/src/jalview/hmmer/rest/PhmmerClient.java
new file mode 100644 (file)
index 0000000..119c805
--- /dev/null
@@ -0,0 +1,204 @@
+package jalview.hmmer.rest;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+
+public class PhmmerClient {
+  private static final String defaultURL =
+      "https://www.ebi.ac.uk/Tools/services/rest/hmmer3_phmmer/";
+  private final URL baseUrl;
+  private final List<String> databaseValues;
+  private final List<String> mxValues;
+  
+  public static String getDefaultURL() {
+    return defaultURL;
+  }
+
+  private PhmmerClient(URL url) throws IOException {
+    this.baseUrl = url;
+    databaseValues = fetchParameterValues("database");
+    mxValues = fetchParameterValues("mx");
+  }
+
+  private List<String> fetchParameterValues(String param) throws IOException {
+    URL url = new URL(baseUrl, "parameterdetails/" + param);
+    Document doc;
+    try {
+      doc = fetchXMLDocument(url);
+    }
+    catch (SAXException e) {
+      throw new IOException("Malformed XML response", e);
+    }
+    Node valuesNode = doc.getElementsByTagName("values").item(0);
+    if (valuesNode == null)
+      throw new IOException(
+          "Missing node /parameter/values.");
+    Node valueNode = valuesNode.getFirstChild();
+    List<String> result = new ArrayList<>();
+    int index = 0;
+    while (valueNode != null) {
+      if (valueNode.getNodeType() != Node.ELEMENT_NODE)
+        throw new IOException(format(
+            "Node /parameter/values/value[%d] is not an element node.", index));
+      Node value = ((Element) valueNode).getElementsByTagName("value").item(0);
+      if (value == null)
+        throw new IOException(format(
+            "Missing node /parameter/values/value[%d]/value.", index));
+      result.add(value.getTextContent());
+      index++;
+      valueNode = valueNode.getNextSibling();
+    }
+    return result;
+  }
+
+  private Document fetchXMLDocument(URL url) throws IOException, SAXException {
+    InputStream stream = url.openStream();
+    try (stream) {
+      return fetchXMLDocument(stream);
+    }
+  }
+
+  private Document fetchXMLDocument(InputStream stream) throws IOException, SAXException {
+    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+    DocumentBuilder builder;
+    try {
+      builder = factory.newDocumentBuilder();
+    }
+    catch (ParserConfigurationException e) {
+      throw new RuntimeException(e);
+    }
+    Document doc = builder.parse(stream);
+    doc.normalize();
+    return doc;
+  }
+
+  public static PhmmerClient create(String url) throws IOException {
+    return new PhmmerClient(new URL(url));
+  }
+
+  public static PhmmerClient create() throws IOException {
+    return new PhmmerClient(new URL(defaultURL));
+  }
+
+  public PhmmerRequestBuilder newRequestBuilder() {
+    return new PhmmerRequestBuilder(mxValues, databaseValues);
+  }
+
+  public String submitRequest(PhmmerRequest request, String email) throws IOException {
+    URL url = new URL(baseUrl, "run/");
+    FormURLEncodedBodyBuilder bodyBuilder = new FormURLEncodedBodyBuilder();
+    bodyBuilder.addParameter("email", email);
+    bodyBuilder.populateFromRequest(request);
+    byte[] body = bodyBuilder.build();
+
+    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+    connection.setDoInput(true);
+    connection.setDoOutput(true);
+    connection.connect();
+    connection.getOutputStream().write(body);
+    int statusCode = connection.getResponseCode();
+    if (statusCode == 200) {
+      var stream = connection.getInputStream();
+      try (stream) {
+        return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
+      }
+    }
+    else if (statusCode == 400) {
+      var stream = connection.getErrorStream();
+      Document doc;
+      try (stream) {
+        doc = fetchXMLDocument(stream);
+      }
+      catch (SAXException e) {
+        throw new IOException("Malformed XML response", e);
+      }
+      Element root = doc.getDocumentElement();
+      if (!root.getNodeName().equals("error"))
+        throw new IOException("Missing node /error.");
+      Node descriptionNode = root.getElementsByTagName("description").item(0);
+      if (descriptionNode == null)
+        throw new IOException("Missing node /error/description.");
+      throw new IOException(descriptionNode.getTextContent());
+    }
+    else {
+      throw new IOException(format("Server returned HTTP response code: %d "
+          + "for URL: %s", statusCode, url.toString()));
+    }
+  }
+
+  public Status pollStatus(String jobId) throws IOException {
+    URL url = new URL(baseUrl, "status/" + jobId);
+    var statusText = new String(url.openStream().readAllBytes(), StandardCharsets.UTF_8);
+    return Status.forStatusText(statusText);
+  }
+
+  public List<ResultType> getResultTypes(String jobId) throws IOException {
+    URL url = new URL(baseUrl, "resulttypes/" + jobId);
+    Document doc;
+    try {
+      doc = fetchXMLDocument(url);
+    }
+    catch (SAXException e) {
+      throw new IOException("Malformed XML response.", e);
+    }
+    Element root = doc.getDocumentElement();
+    root.normalize();
+    List<ResultType> resultTypeList = new ArrayList<>();
+    Node typeNode = root.getFirstChild();
+    while (typeNode != null) {
+      if (typeNode.getNodeType() == Node.ELEMENT_NODE &&
+          typeNode.getNodeName().equals("type")) {
+        var node = typeNode.getFirstChild();
+        var resultType = new ResultType();
+        while (node != null) {
+          switch (node.getNodeName()) {
+          case "description":
+            resultType.description = node.getTextContent();
+            break;
+          case "fileSuffix":
+            resultType.fileSuffix = node.getTextContent();
+            break;
+          case "identifier":
+            resultType.identifier = node.getTextContent();
+            break;
+          case "label":
+            resultType.label = node.getTextContent();
+            break;
+          case "mediaType":
+            resultType.mediaType = node.getTextContent();
+            break;
+          }
+          node = node.getNextSibling();
+        }
+        resultTypeList.add(resultType);
+      }
+      typeNode = typeNode.getNextSibling();
+    }
+    return resultTypeList;
+  }
+
+  public InputStream getFileStream(String jobId, ResultType resultType) throws IOException {
+    return getFileStream(jobId, resultType.getIdentifier());
+  }
+
+  public InputStream getFileStream(String jobId, String identifier) throws IOException {
+    URL url = new URL(baseUrl, "result/" + jobId + "/" + identifier);
+    return url.openStream();
+  }
+}
diff --git a/src/jalview/hmmer/rest/PhmmerRequest.java b/src/jalview/hmmer/rest/PhmmerRequest.java
new file mode 100644 (file)
index 0000000..bdba85b
--- /dev/null
@@ -0,0 +1,102 @@
+package jalview.hmmer.rest;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public final class PhmmerRequest {
+  private Optional<Float> incE;
+  private Optional<Float> incdomE;
+  private Optional<Float> E;
+  private Optional<Float> domE;
+  private Optional<Float> incT;
+  private Optional<Float> incdomT;
+  private Optional<Float> T;
+  private Optional<Float> domT;
+  private Optional<Float> popen;
+  private Optional<Float> pextend;
+  private Optional<String> mx;
+  private Optional<Boolean> noBias;
+  private Optional<Boolean> compressedOut;
+  private Optional<Boolean> alignView;
+  private String database;
+  private Optional<Float> evalue;
+  private InputSequence sequence;
+  private Optional<Integer> nhits;
+
+  public PhmmerRequest(
+      Float incE,
+      Float incdomE,
+      Float E,
+      Float domE,
+      Float incT,
+      Float incdomT,
+      Float T,
+      Float domT,
+      Float popen,
+      Float pextend,
+      String mx,
+      Boolean noBias,
+      Boolean compressedOut,
+      Boolean alignView,
+      String database,
+      Float evalue,
+      InputSequence sequence,
+      Integer nhits) {
+    Objects.requireNonNull(database);
+    Objects.requireNonNull(sequence);
+    this.incE = Optional.ofNullable(incE);
+    this.incdomE = Optional.ofNullable(incdomE);
+    this.E = Optional.ofNullable(E);
+    this.domE = Optional.ofNullable(domE);
+    this.incT = Optional.ofNullable(incT);
+    this.incdomT = Optional.ofNullable(incdomT);
+    this.T = Optional.ofNullable(T);
+    this.domT = Optional.ofNullable(domT);
+    this.popen = Optional.ofNullable(popen);
+    this.pextend = Optional.ofNullable(pextend);
+    this.mx = Optional.ofNullable(mx);
+    this.noBias = Optional.ofNullable(noBias);
+    this.compressedOut = Optional.ofNullable(compressedOut);
+    this.alignView = Optional.ofNullable(alignView);
+    this.database = database;
+    this.evalue = Optional.ofNullable(evalue);
+    this.sequence = sequence;
+    this.nhits = Optional.ofNullable(nhits);
+  }
+
+  public Optional<Float> getIncE() { return incE; }
+
+  public Optional<Float> getIncdomE() { return incdomE; }
+
+  public Optional<Float> getE() { return E; }
+
+  public Optional<Float> getDomE() { return domE; }
+
+  public Optional<Float> getIncT() { return incT; }
+
+  public Optional<Float> getIncdomT() { return incdomT; }
+
+  public Optional<Float> getT() { return T; }
+
+  public Optional<Float> getDomT() { return domT; }
+
+  public Optional<Float> getPopen() { return popen; }
+
+  public Optional<Float> getPextend() { return pextend; }
+
+  public Optional<String> getMx() { return mx; }
+
+  public Optional<Boolean> getNoBias() { return noBias; }
+
+  public Optional<Boolean> getCompressedOut() { return compressedOut; }
+
+  public Optional<Boolean> getAlignView() { return alignView; }
+
+  public String getDatabase() { return database; }
+
+  public Optional<Float> getEvalue() { return evalue; }
+
+  public InputSequence getSequence() { return sequence; }
+
+  public Optional<Integer> getNhits() { return nhits; }
+}
diff --git a/src/jalview/hmmer/rest/PhmmerRequestBuilder.java b/src/jalview/hmmer/rest/PhmmerRequestBuilder.java
new file mode 100644 (file)
index 0000000..60f5546
--- /dev/null
@@ -0,0 +1,314 @@
+package jalview.hmmer.rest;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNullElse;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class PhmmerRequestBuilder {
+  // Default values
+  private float defaultIncE = 0.01f;
+  private float defaultIncdomE = 0.03f;
+  private float defaultE = 1.0f;
+  private float defaultDomE = 1.0f;
+
+  private float defaultIncT = 25.0f;
+  private float defaultIncdomT = 22.0f;
+  private float defaultT = 7.0f;
+  private float defaultDomT = 5.0f;
+
+  private float defaultPopen = 0.02f;
+  private float defaultPextend = 0.4f;
+  private String defaultMx = "BLOSUM62";
+  private float defaultEvalue = 0.01f;
+
+  private boolean defaultNoBias = false;
+  private boolean defaultCompressedOut = false;
+  private boolean defaultAlignView = true;
+  private String defaultDatabase = "uniprotkb";
+  private int defaultNhits = 100;
+
+  // Current values
+  private Float incE = null;
+  private Float incdomE = null;
+  private Float E = null;
+  private Float domE = null;
+
+  private Float incT = null;
+  private Float incdomT = null;
+  private Float T = null;
+  private Float domT = null;
+
+  private Float popen = null;
+  private Float pextend = null;
+  private String mx = null;
+  private Float evalue = null;
+
+  private Boolean noBias = null;
+  private Boolean compressedOut = null;
+  private Boolean alignView = null;
+  private String database = null;
+  private InputSequence sequence = null;
+  private Integer nhits = null;
+
+  private final List<String> allowedMx;
+  private final List<String> allowedDatabase;
+
+  PhmmerRequestBuilder(List<String> allowedMx,
+      List<String> allowedDatabase) {
+    this.allowedMx = Collections.unmodifiableList(allowedMx);
+    this.allowedDatabase = Collections.unmodifiableList(allowedDatabase);
+  }
+
+  public List<String> getAllowedMxValues() { return allowedMx; }
+  public List<String> getAllowedDatabaseValues() { return allowedDatabase; }
+
+  private static class FileInputSequence implements InputSequence {
+    private final File file;
+
+    private FileInputSequence(File file) {
+      this.file = file;
+    }
+
+    @Override
+    public String getAsString() throws IOException {
+      Reader reader = new FileReader(file);
+      StringBuilder builder = new StringBuilder();
+      try (reader) {
+        char[] buffer = new char[1024 * 16];
+        int bytesRead;
+        while ((bytesRead = reader.read(buffer)) >= 0) {
+          builder.append(buffer, 0, bytesRead);
+        }
+      }
+      return builder.toString();
+    }
+  }
+
+  private static class StringInputSequence implements InputSequence {
+    private final String str;
+
+    private StringInputSequence(String str) {
+      this.str = str;
+    }
+
+    @Override
+    public String getAsString() throws IOException { return str; }
+  }
+
+  public float getDefaultIncE() { return defaultIncE; }
+
+  public float getDefaultIncdomE() { return defaultIncdomE; }
+
+  public float getDefaultE() { return defaultE; }
+
+  public float getDefaultDomE() { return defaultDomE; }
+
+  public float getDefaultIncT() { return defaultIncT; }
+
+  public float getDefaultIncdomT() { return defaultIncdomT; }
+
+  public float getDefaultT() { return defaultT; }
+
+  public float getDefaultDomT() { return defaultDomT; }
+
+  public float getDefaultPopen() { return defaultPopen; }
+
+  public float getDefaultPextend() { return defaultPextend; }
+
+  public String getDefaultMx() { return defaultMx; }
+
+  public float getDefaultEvalue() { return defaultEvalue; }
+
+  public boolean getDefaultNoBias() { return defaultNoBias; }
+
+  public boolean getDefaultCompressedOut() { return defaultCompressedOut; }
+
+  public boolean getDefaultAlignView() { return defaultAlignView; }
+
+  public String getDefaultDatabase() { return defaultDatabase; }
+
+  public int getDefaultNhits() { return defaultNhits; }
+
+  public PhmmerRequestBuilder incE(Float incE) {
+    if (incE != null && (incE <= 0 || incE > 10))
+      throw new IllegalArgumentException(format("incE must be greater than 0 "
+          + "and less or equal to 10, value: %f", incE));
+    this.incE = incE;
+    return this;
+  }
+
+  public PhmmerRequestBuilder incdomE(Float incdomE) {
+    if (incdomE != null && (incdomE <= 0 || incdomE > 10))
+      throw new IllegalArgumentException(format("incdomE must be greater than 0 "
+          + "and less or equal to 10, value: %f", incdomE));
+    this.incdomE = incdomE;
+    return this;
+  }
+
+  public PhmmerRequestBuilder E(Float E) {
+    if (E != null && (E <= 0 || E > 10))
+      throw new IllegalArgumentException(format("E must be greater than 0 "
+          + "and less or equal to 10, value: %f", E));
+    this.E = E;
+    return this;
+  }
+
+  public PhmmerRequestBuilder domE(Float domE) {
+    if (domE != null && (domE <= 0 || domE > 10))
+      throw new IllegalArgumentException(format("domE must be greater than 0 "
+          + "and less or equal to 10, value: %f", domE));
+    this.domE = domE;
+    return this;
+  }
+
+  public PhmmerRequestBuilder incT(Float incT) {
+    if (incT != null && incT <= 0)
+      throw new IllegalArgumentException(format("incT must be greater than 0, "
+          + "value: %f", incT));
+    this.incT = incT;
+    return this;
+  }
+
+  public PhmmerRequestBuilder incdomT(Float incdomT) {
+    if (incdomT != null && incdomT <= 0)
+      throw new IllegalArgumentException(format("incdomT must be greater than 0, "
+          + "value: %f", incdomT));
+    this.incdomT = incdomT;
+    return this;
+  }
+
+  public PhmmerRequestBuilder T(Float T) {
+    if (T != null && T <= 0 )
+      throw new IllegalArgumentException(format("T must be greater than 0, "
+          + "value: %f", T));
+    this.T = T;
+    return this;
+  }
+
+  public PhmmerRequestBuilder domT(Float domT) {
+    if (domT != null && domT <= 0)
+      throw new IllegalArgumentException(format("domT must be greater than 0, "
+          + "value: %f", domT));
+    this.domT = domT;
+    return this;
+  }
+
+  public PhmmerRequestBuilder popen(Float popen) {
+    if (popen != null && (popen < 0 || popen >= 0.5f))
+      throw new IllegalArgumentException(format("popen must be greater or "
+          + "equal to 0 and less than 0.5, value: %s", popen));
+    this.popen = popen;
+    return this;
+  }
+
+  public PhmmerRequestBuilder pextend(Float pextend) {
+    if (pextend != null && (pextend < 0 || pextend >= 1))
+      throw new IllegalArgumentException(format("pextend must be greater or "
+          + "equal to 0 and less than 1, value: %f", pextend));
+    this.pextend = pextend;
+    return this;
+  }
+
+  public PhmmerRequestBuilder mx(String mx) {
+    if (mx != null && !allowedMx.contains(mx))
+      throw new IllegalArgumentException(String.format(
+          "\"%s\" is not one of the allowed values %s",
+          mx, allowedMx.toString()));
+    this.mx = mx;
+    return this;
+  }
+
+  public PhmmerRequestBuilder noBias(Boolean noBias) {
+    this.noBias = noBias;
+    return this;
+  }
+
+  public PhmmerRequestBuilder compressedOut(Boolean compressedOut) {
+    this.compressedOut = compressedOut;
+    return this;
+  };
+
+  public PhmmerRequestBuilder alignView(Boolean alignView) {
+    this.alignView = alignView;
+    return this;
+  }
+
+  public PhmmerRequestBuilder database(String database) {
+    Objects.requireNonNull(database);
+    if (!allowedDatabase.contains(database))
+      throw new IllegalArgumentException(String.format(
+          "\"%s\" is not one of the allowed values: %s",
+          database, allowedDatabase.toString()));
+    this.database = database;
+    return this;
+  }
+
+  public PhmmerRequestBuilder evalue(Float evalue) {
+    this.evalue = evalue;
+    return this;
+  }
+
+  public PhmmerRequestBuilder sequenceString(String sequence) {
+    Objects.requireNonNull(sequence);
+    this.sequence = new StringInputSequence(sequence);
+    return this;
+  }
+
+  public PhmmerRequestBuilder sequenceFile(String path) {
+    Objects.requireNonNull(path);
+    this.sequence = new FileInputSequence(new File(path));
+    return this;
+  }
+
+  public PhmmerRequestBuilder sequenceFile(File file) {
+    Objects.requireNonNull(file);
+    this.sequence = new FileInputSequence(file);
+    return this;
+  }
+
+  public PhmmerRequestBuilder nhits(Integer nhits) {
+    this.nhits = nhits;
+    return this;
+  }
+
+  public PhmmerRequest build() {
+    if (sequence == null)
+      throw new IllegalStateException("sequence not set");
+    if (database == null)
+      throw new IllegalStateException("database not set");
+    boolean usingEValues = incE != null || incdomE != null || E != null || domE != null;
+    boolean usingBitScores = incT != null || incdomT != null || T != null || domT != null;
+    if (usingEValues && usingBitScores)
+      throw new IllegalStateException("using both E-values and bit scores is not allowed");
+    return new PhmmerRequest(
+        valueOrDefaultIfEnabled(incE, defaultIncE, usingEValues),
+        valueOrDefaultIfEnabled(incdomE, defaultIncdomE, usingEValues),
+        valueOrDefaultIfEnabled(E, defaultE, usingEValues),
+        valueOrDefaultIfEnabled(domE, defaultDomE, usingEValues),
+        valueOrDefaultIfEnabled(incT, defaultIncT, usingBitScores),
+        valueOrDefaultIfEnabled(incdomT, defaultIncdomT, usingBitScores),
+        valueOrDefaultIfEnabled(T, defaultT, usingBitScores),
+        valueOrDefaultIfEnabled(domT, defaultDomT, usingBitScores),
+        requireNonNullElse(popen, defaultPopen),
+        requireNonNullElse(pextend, defaultPextend),
+        requireNonNullElse(mx, defaultMx),
+        requireNonNullElse(noBias, defaultNoBias),
+        requireNonNullElse(compressedOut, defaultCompressedOut),
+        requireNonNullElse(alignView, defaultAlignView),
+        database,
+        requireNonNullElse(evalue, defaultEvalue),
+        sequence,
+        requireNonNullElse(nhits, defaultNhits));
+  }
+
+  private static <T> T valueOrDefaultIfEnabled(T obj, T defaultObj, boolean enable) {
+    return enable ? ((obj != null) ? obj : defaultObj) : null;
+  }
+}
diff --git a/src/jalview/hmmer/rest/ResultType.java b/src/jalview/hmmer/rest/ResultType.java
new file mode 100644 (file)
index 0000000..ee9cc1d
--- /dev/null
@@ -0,0 +1,27 @@
+package jalview.hmmer.rest;
+
+public final class ResultType {
+  String description;
+  String fileSuffix;
+  String identifier;
+  String label;
+  String mediaType;
+
+  ResultType() {}
+
+  public String getDescription() { return description; }
+
+  public String getFileSuffix() { return fileSuffix; }
+
+  public String getIdentifier() { return identifier; }
+
+  public String getLabel() { return label; }
+
+  public String getMediaType() { return mediaType; }
+
+  public String toString() {
+    return String.format(
+        "ResultType(identifier=%s, fileSuffix=%s, label=%s, mediaType=%s)",
+        identifier, fileSuffix, label, mediaType);
+  }
+}
diff --git a/src/jalview/hmmer/rest/Status.java b/src/jalview/hmmer/rest/Status.java
new file mode 100644 (file)
index 0000000..939e91e
--- /dev/null
@@ -0,0 +1,29 @@
+package jalview.hmmer.rest;
+
+import java.util.HashMap;
+
+public enum Status {
+  PENDING("PENDING"),
+  RUNNING("RUNNING"),
+  FINISHED("FINISHED"),
+  FAILURE("FAILURE"),
+  NOT_FOUND("NOT_FOUND"),
+  UNDEFINED("UNDEFINED");
+
+  String statusText;
+
+  Status(String text) {
+    this.statusText = text;
+  }
+
+  static final HashMap<String, Status> mapping = new HashMap<>();
+  static {
+    for (var value : Status.values()) {
+      mapping.put(value.statusText, value);
+    }
+  }
+
+  public static Status forStatusText(String text) {
+    return mapping.getOrDefault(text, UNDEFINED);
+  }
+}