--- /dev/null
+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()));
+ }
+}
--- /dev/null
+package jalview.hmmer.rest;
+
+import java.io.IOException;
+
+public interface InputSequence {
+ String getAsString() throws IOException;
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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; }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}