From: Mateusz Warowny Date: Tue, 8 Feb 2022 19:57:41 +0000 (+0100) Subject: JAL-3954 Introduce hmmer rest client to jalview. X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=716ff28566d6a21a801aa06781679d87388adf74;p=jalview.git JAL-3954 Introduce hmmer rest client to jalview. --- diff --git a/src/jalview/hmmer/rest/FormURLEncodedBodyBuilder.java b/src/jalview/hmmer/rest/FormURLEncodedBodyBuilder.java new file mode 100644 index 0000000..04359a1 --- /dev/null +++ b/src/jalview/hmmer/rest/FormURLEncodedBodyBuilder.java @@ -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> entries = new ArrayList<>(); + + public void addParameter(String parameter, String value) { + entries.add(new SimpleEntry(parameter, value)); + } + + public byte[] build() { + StringBuilder builder = new StringBuilder(); + var iter = entries.iterator(); + Map.Entry 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 index 0000000..3735760 --- /dev/null +++ b/src/jalview/hmmer/rest/InputSequence.java @@ -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 index 0000000..119c805 --- /dev/null +++ b/src/jalview/hmmer/rest/PhmmerClient.java @@ -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 databaseValues; + private final List mxValues; + + public static String getDefaultURL() { + return defaultURL; + } + + private PhmmerClient(URL url) throws IOException { + this.baseUrl = url; + databaseValues = fetchParameterValues("database"); + mxValues = fetchParameterValues("mx"); + } + + private List 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 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 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 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 index 0000000..bdba85b --- /dev/null +++ b/src/jalview/hmmer/rest/PhmmerRequest.java @@ -0,0 +1,102 @@ +package jalview.hmmer.rest; + +import java.util.Objects; +import java.util.Optional; + +public final class PhmmerRequest { + private Optional incE; + private Optional incdomE; + private Optional E; + private Optional domE; + private Optional incT; + private Optional incdomT; + private Optional T; + private Optional domT; + private Optional popen; + private Optional pextend; + private Optional mx; + private Optional noBias; + private Optional compressedOut; + private Optional alignView; + private String database; + private Optional evalue; + private InputSequence sequence; + private Optional 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 getIncE() { return incE; } + + public Optional getIncdomE() { return incdomE; } + + public Optional getE() { return E; } + + public Optional getDomE() { return domE; } + + public Optional getIncT() { return incT; } + + public Optional getIncdomT() { return incdomT; } + + public Optional getT() { return T; } + + public Optional getDomT() { return domT; } + + public Optional getPopen() { return popen; } + + public Optional getPextend() { return pextend; } + + public Optional getMx() { return mx; } + + public Optional getNoBias() { return noBias; } + + public Optional getCompressedOut() { return compressedOut; } + + public Optional getAlignView() { return alignView; } + + public String getDatabase() { return database; } + + public Optional getEvalue() { return evalue; } + + public InputSequence getSequence() { return sequence; } + + public Optional getNhits() { return nhits; } +} diff --git a/src/jalview/hmmer/rest/PhmmerRequestBuilder.java b/src/jalview/hmmer/rest/PhmmerRequestBuilder.java new file mode 100644 index 0000000..60f5546 --- /dev/null +++ b/src/jalview/hmmer/rest/PhmmerRequestBuilder.java @@ -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 allowedMx; + private final List allowedDatabase; + + PhmmerRequestBuilder(List allowedMx, + List allowedDatabase) { + this.allowedMx = Collections.unmodifiableList(allowedMx); + this.allowedDatabase = Collections.unmodifiableList(allowedDatabase); + } + + public List getAllowedMxValues() { return allowedMx; } + public List 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 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 index 0000000..ee9cc1d --- /dev/null +++ b/src/jalview/hmmer/rest/ResultType.java @@ -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 index 0000000..939e91e --- /dev/null +++ b/src/jalview/hmmer/rest/Status.java @@ -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 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); + } +}