2 * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.0b1)
3 * Copyright (C) 2014 The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
11 * Jalview is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
14 * PURPOSE. See the GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along with Jalview. If not, see <http://www.gnu.org/licenses/>.
17 * The Jalview Authors are detailed in the 'AUTHORS' file.
19 package jalview.ws.rest;
21 import jalview.datamodel.SequenceI;
22 import jalview.io.packed.DataProvider.JvDataType;
23 import jalview.ws.rest.params.Alignment;
24 import jalview.ws.rest.params.AnnotationFile;
25 import jalview.ws.rest.params.SeqGroupIndexVector;
28 import java.util.ArrayList;
29 import java.util.HashMap;
30 import java.util.Hashtable;
31 import java.util.List;
33 import java.util.NoSuchElementException;
34 import java.util.StringTokenizer;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
38 public class RestServiceDescription
41 * create a new rest service description ready to be configured
43 public RestServiceDescription()
57 public RestServiceDescription(String action, String description,
58 String name, String postUrl, String urlSuffix,
59 Map<String, InputType> inputParams, boolean hseparable,
60 boolean vseparable, char gapCharacter)
63 this.details = new UIinfo();
64 details.Action = action == null ? "" : action;
65 details.description = description == null ? "" : description;
66 details.Name = name == null ? "" : name;
67 this.postUrl = postUrl == null ? "" : postUrl;
68 this.urlSuffix = urlSuffix == null ? "" : urlSuffix;
69 if (inputParams != null)
71 this.inputParams = inputParams;
73 this.hseparable = hseparable;
74 this.vseparable = vseparable;
75 this.gapCharacter = gapCharacter;
78 public boolean equals(Object o)
80 if (o == null || !(o instanceof RestServiceDescription))
84 RestServiceDescription other = (RestServiceDescription) o;
85 boolean diff = (gapCharacter != other.gapCharacter);
86 diff |= vseparable != other.vseparable;
87 diff |= hseparable != other.hseparable;
88 diff |= !(urlSuffix.equals(other.urlSuffix));
89 // TODO - robust diff that includes constants and reordering of URL
90 // diff |= !(postUrl.equals(other.postUrl));
91 // diff |= !inputParams.equals(other.inputParams);
92 diff |= !details.Name.equals(other.details.Name);
93 diff |= !details.Action.equals(other.details.Action);
94 diff |= !details.description.equals(other.details.description);
99 * Service UI Info { Action, Specific Name of Service, Brief Description }
104 public String getAction()
109 public void setAction(String action)
114 public String getName()
119 public void setName(String name)
124 public String getDescription()
129 public void setDescription(String description)
131 this.description = description;
141 public UIinfo details = new UIinfo();
143 public String getAction()
145 return details.getAction();
148 public void setAction(String action)
150 details.setAction(action);
153 public String getName()
155 return details.getName();
158 public void setName(String name)
160 details.setName(name);
163 public String getDescription()
165 return details.getDescription();
168 public void setDescription(String description)
170 details.setDescription(description);
178 public String getPostUrl()
183 public void setPostUrl(String postUrl)
185 this.postUrl = postUrl;
188 public String getUrlSuffix()
193 public void setUrlSuffix(String urlSuffix)
195 this.urlSuffix = urlSuffix;
198 public Map<String, InputType> getInputParams()
203 public void setInputParams(Map<String, InputType> inputParams)
205 this.inputParams = inputParams;
208 public void setHseparable(boolean hseparable)
210 this.hseparable = hseparable;
213 public void setVseparable(boolean vseparable)
215 this.vseparable = vseparable;
218 public void setGapCharacter(char gapCharacter)
220 this.gapCharacter = gapCharacter;
224 * suffix that should be added to any url used if it does not already end in
230 * input info given as key/value pairs - mapped to post arguments
232 Map<String, InputType> inputParams = new HashMap<String, InputType>();
235 * assigns the given inputType it to its corresponding input parameter token
240 public void setInputParam(InputType it)
242 inputParams.put(it.token, it);
246 * remove the given input type it from the set of service input parameters.
250 public void removeInputParam(InputType it)
252 inputParams.remove(it.token);
256 * service requests alignment data
261 * service requests alignment and/or seuqence annotationo data
266 * service requests partitions defined over input (alignment) data
268 boolean partitiondata;
271 * process ths input data and set the appropriate shorthand flags describing
272 * the input the service wants
274 public void setInvolvesFlags()
276 aligndata = inputInvolves(Alignment.class);
277 annotdata = inputInvolves(AnnotationFile.class);
278 partitiondata = inputInvolves(SeqGroupIndexVector.class);
282 * Service return info { alignment, annotation file (loaded back on to
283 * alignment), tree (loaded back on to alignment), sequence annotation -
284 * loaded back on to alignment), text report, pdb structures with sequence
290 * Start with bare minimum: input is alignment + groups on alignment
296 private String invalidMessage = null;
299 * parse the given linkString of the form '<label>|<url>|separator
300 * char[|optional sequence separator char]' into parts. url may contain a
301 * string $SEQUENCEIDS<=optional regex=>$ where <=optional regex=> must be of
302 * the form =/<perl style regex>/=$ or $SEQUENCES<=optional regex=>$ or
303 * $SEQUENCES<=optional regex=>$.
307 public RestServiceDescription(String link)
309 StringBuffer warnings = new StringBuffer();
310 if (!configureFromEncodedString(link, warnings))
312 if (warnings.length() > 0)
314 invalidMessage = warnings.toString();
319 public RestServiceDescription(RestServiceDescription toedit)
321 // Rather then do the above, we cheat and use our human readable
322 // serialization code to clone everything
323 this(toedit.toString());
325 * if (toedit == null) { return; } /** urlSuffix = toedit.urlSuffix; postUrl
326 * = toedit.postUrl; hseparable = toedit.hseparable; vseparable =
327 * toedit.vseparable; gapCharacter = toedit.gapCharacter; details = new
328 * RestServiceDescription.UIinfo(); details.Action = toedit.details.Action;
329 * details.description = toedit.details.description; details.Name =
330 * toedit.details.Name; for (InputType itype: toedit.inputParams.values()) {
331 * inputParams.put(itype.token, itype.clone());
335 // TODO Implement copy constructor NOW*/
339 * @return the invalidMessage
341 public String getInvalidMessage()
343 return invalidMessage;
347 * Check if URL string was parsed properly.
349 * @return boolean - if false then <code>getInvalidMessage</code> returns an
352 public boolean isValid()
354 return invalidMessage == null;
357 private static boolean debug = false;
360 * parse the string into a list
364 * @return elements separated by separator
366 public static String[] separatorListToArray(String list, String separator)
368 int seplen = separator.length();
369 if (list == null || list.equals("") || list.equals(separator))
371 java.util.ArrayList<String> jv = new ArrayList<String>();
372 int cp = 0, pos, escape;
373 boolean wasescaped = false, wasquoted = false;
374 String lstitem = null;
375 while ((pos = list.indexOf(separator, cp)) >= cp)
378 escape = (pos > 0 && list.charAt(pos - 1) == '\\') ? -1 : 0;
379 if (wasescaped || wasquoted)
381 // append to previous pos
382 jv.set(jv.size() - 1,
383 lstitem = lstitem + separator
384 + list.substring(cp, pos + escape));
389 jv.add(lstitem = list.substring(cp, pos + escape));
392 wasescaped = escape == -1;
395 // last separator may be in an unmatched quote
396 if (java.util.regex.Pattern.matches("('[^']*')*[^']*'", lstitem))
403 if (cp < list.length())
405 String c = list.substring(cp);
406 if (wasescaped || wasquoted)
408 // append final separator
409 jv.set(jv.size() - 1, lstitem + separator + c);
413 if (!c.equals(separator))
421 String[] v = jv.toArray(new String[jv.size()]);
425 System.err.println("Array from '" + separator
426 + "' separated List:\n" + v.length);
427 for (int i = 0; i < v.length; i++)
429 System.err.println("item " + i + " '" + v[i] + "'");
436 System.err.println("Empty Array from '" + separator
437 + "' separated List");
443 * concatenate the list with separator
447 * @return concatenated string
449 public static String arrayToSeparatorList(String[] list, String separator)
451 StringBuffer v = new StringBuffer();
452 if (list != null && list.length > 0)
454 for (int i = 0, iSize = list.length; i < iSize; i++)
462 // TODO - escape any separator values in list[i]
468 System.err.println("Returning '" + separator
469 + "' separated List:\n");
470 System.err.println(v);
476 System.err.println("Returning empty '" + separator
477 + "' separated List\n");
479 return "" + separator;
483 * parse a string containing a list of service properties and configure the
484 * service description
487 * param warnings a StringBuffer that any warnings about invalid
488 * content will be appended to.
490 private boolean configureFromServiceInputProperties(String propList,
491 StringBuffer warnings)
493 String[] props = separatorListToArray(propList, ",");
499 boolean valid = true;
501 int l = warnings.length();
503 for (String prop : props)
505 if ((i = prop.indexOf("=")) > -1)
507 val = prop.substring(i + 1);
508 if (val.startsWith("\'") && val.endsWith("\'"))
510 val = val.substring(1, val.length() - 1);
512 prop = prop.substring(0, i);
515 if (prop.equals("hseparable"))
519 if (prop.equals("vseparable"))
523 if (prop.equals("gapCharacter"))
525 if (val == null || val.length() == 0 || val.length() > 1)
528 warnings.append((warnings.length() > 0 ? "\n" : "")
529 + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
534 gapCharacter = val.charAt(0);
537 if (prop.equals("returns"))
539 _configureOutputFormatFrom(val, warnings);
542 // return true if valid is true and warning buffer was not appended to.
543 return valid && (l == warnings.length());
546 private String _genOutputFormatString()
549 if (resultData == null)
553 for (JvDataType type : resultData)
555 if (buff.length() > 0)
559 buff += type.toString();
564 private void _configureOutputFormatFrom(String outstring,
565 StringBuffer warnings)
567 if (outstring.indexOf(";") == -1)
569 // we add a token, for simplicity
570 outstring = outstring + ";";
572 StringTokenizer st = new StringTokenizer(outstring, ";");
574 resultData = new ArrayList<JvDataType>();
575 while (st.hasMoreTokens())
579 resultData.add(JvDataType.valueOf(tok = st.nextToken()));
580 } catch (NoSuchElementException x)
582 warnings.append("Invalid result type: '" + tok
583 + "' (must be one of: ");
585 for (JvDataType vl : JvDataType.values())
587 warnings.append(sep);
588 warnings.append(vl.toString());
591 warnings.append(" separated by semi-colons)\n");
596 private String getServiceIOProperties()
598 ArrayList<String> vls = new ArrayList<String>();
601 vls.add("hseparable");
606 vls.add("vseparable");
609 vls.add(new String("gapCharacter='" + gapCharacter + "'"));
610 vls.add(new String("returns='" + _genOutputFormatString() + "'"));
611 return arrayToSeparatorList(vls.toArray(new String[0]), ",");
614 public String toString()
616 StringBuffer result = new StringBuffer();
618 result.append(details.Name);
620 result.append(details.Action);
622 if (details.description != null)
624 result.append(details.description);
627 // list job input flags
629 result.append(getServiceIOProperties());
630 // list any additional cgi parameters needed for result retrieval
631 if (urlSuffix != null && urlSuffix.length() > 0)
634 result.append(urlSuffix);
637 result.append(getInputParamEncodedUrl());
638 return result.toString();
642 * processes a service encoded as a string (as generated by
643 * RestServiceDescription.toString()) Note - this will only use the first
644 * service definition encountered in the string to configure the service.
648 * - where warning messages are reported.
649 * @return true if configuration was parsed successfully.
651 public boolean configureFromEncodedString(String encoding,
652 StringBuffer warnings)
654 String[] list = separatorListToArray(encoding, "|");
656 int nextpos = parseServiceList(list, warnings, 0);
665 * processes the given list from position p, attempting to configure the
666 * service from it. Service lists are formed by concatenating individual
667 * stringified services. The first character of a stringified service is '|',
668 * enabling this, and the parser will ignore empty fields in a '|' separated
669 * list when they fall outside a service definition.
676 protected int parseServiceList(String[] list, StringBuffer warnings, int p)
678 boolean invalid = false;
679 // look for the first non-empty position - expect it to be service name
680 while (list[p] != null && list[p].trim().length() == 0)
684 details.Name = list[p];
685 details.Action = list[p + 1];
686 details.description = list[p + 2];
687 invalid |= !configureFromServiceInputProperties(list[p + 3], warnings);
688 if (list.length - p > 5 && list[p + 5] != null
689 && list[p + 5].trim().length() > 5)
691 urlSuffix = list[p + 4];
692 invalid |= !configureFromInputParamEncodedUrl(list[p + 5], warnings);
697 if (list.length - p > 4 && list[p + 4] != null
698 && list[p + 4].trim().length() > 5)
701 invalid |= !configureFromInputParamEncodedUrl(list[p + 4], warnings);
705 return invalid ? -1 : p;
709 * @return string representation of the input parameters, their type and
710 * constraints, appended to the service's base submission URL
712 private String getInputParamEncodedUrl()
714 StringBuffer url = new StringBuffer();
715 if (postUrl == null || postUrl.length() < 5)
721 char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
722 boolean consts = true;
725 for (Map.Entry<String, InputType> param : inputParams.entrySet())
727 List<String> vals = param.getValue().getURLEncodedParameter();
728 if (param.getValue().isConstant())
732 url.append(appendChar);
734 url.append(param.getValue().token);
735 if (vals.size() == 1)
738 url.append(vals.get(0));
746 url.append(appendChar);
748 url.append(param.getValue().token);
750 // write parameter set as $TOKENPREFIX:csv list of params$ for this
753 url.append(param.getValue().getURLtokenPrefix());
755 url.append(arrayToSeparatorList(vals.toArray(new String[0]),
762 // toggle consts and repeat until !consts is false:
763 } while (!(consts = !consts));
764 return url.toString();
768 * parse the service URL and input parameters from the given encoded URL
769 * string and configure the RestServiceDescription from it.
774 * @return true if URL parsed correctly. false means the configuration failed.
776 private boolean configureFromInputParamEncodedUrl(String ipurl,
777 StringBuffer warnings)
779 boolean valid = true;
781 String url = new String();
782 Matcher prms = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$")
784 Map<String, InputType> iparams = new Hashtable<String, InputType>();
788 if (lastp < prms.start(0))
790 url += ipurl.substring(lastp, prms.start(0));
791 lastp = prms.end(0) + 1;
793 String sep = prms.group(1);
794 String tok = prms.group(2);
795 String iprm = prms.group(3);
796 int colon = iprm.indexOf(":");
797 String iprmparams = "";
800 iprmparams = iprm.substring(colon + 1);
801 iprm = iprm.substring(0, colon);
803 valid = parseTypeString(prms.group(0), tok, iprm, iprmparams,
810 URL u = new URL(url);
812 inputParams = iparams;
813 } catch (Exception e)
815 warnings.append("Failed to parse '" + url + "' as a URL.\n");
822 public static Class[] getInputTypes()
824 // TODO - find a better way of maintaining this classlist
826 { jalview.ws.rest.params.Alignment.class,
827 jalview.ws.rest.params.AnnotationFile.class,
828 SeqGroupIndexVector.class,
829 jalview.ws.rest.params.SeqIdVector.class,
830 jalview.ws.rest.params.SeqVector.class,
831 jalview.ws.rest.params.Tree.class };
834 public static boolean parseTypeString(String fullstring, String tok,
835 String iprm, String iprmparams, Map<String, InputType> iparams,
836 StringBuffer warnings)
838 boolean valid = true;
840 for (Class type : getInputTypes())
844 jinput = (InputType) (type.getConstructor().newInstance(null));
845 if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
847 ArrayList<String> al = new ArrayList<String>();
848 for (String prprm : separatorListToArray(iprmparams, ","))
850 // hack to ensure that strings like "sep=','" containing unescaped
851 // commas as values are concatenated
852 al.add(prprm.trim());
854 if (!jinput.configureFromURLtokenString(al, warnings))
857 warnings.append("Failed to parse '" + fullstring + "' as a "
858 + jinput.getURLtokenPrefix() + " input tag.\n");
863 iparams.put(tok, jinput);
869 } catch (Throwable thr)
878 * covenience method to generate the id and sequence string vector from a set
879 * of seuqences using each sequence's getName() and getSequenceAsString()
883 * @return String[][] {{sequence ids},{sequence strings}}
885 public static String[][] formStrings(SequenceI[] seqs)
887 String[][] idset = new String[2][seqs.length];
888 for (int i = 0; i < seqs.length; i++)
890 idset[0][i] = seqs[i].getName();
891 idset[1][i] = seqs[i].getSequenceAsString();
897 * can this service be run on the visible portion of an alignment regardless
898 * of hidden boundaries ?
900 boolean hseparable = false;
902 boolean vseparable = false;
904 public boolean isHseparable()
913 public boolean isVseparable()
919 * search the input types for an instance of the given class
921 * @param <validInput.inputType> class1
924 public boolean inputInvolves(Class<?> class1)
926 assert (InputType.class.isAssignableFrom(class1));
927 for (InputType val : inputParams.values())
929 if (class1.isAssignableFrom(val.getClass()))
937 char gapCharacter = '-';
941 * @return the preferred gap character for alignments input/output by this
944 public char getGapCharacter()
949 public String getDecoratedResultUrl(String jobId)
951 // TODO: correctly write ?/& appropriate to result URL format.
952 return jobId + urlSuffix;
955 private List<JvDataType> resultData = new ArrayList<JvDataType>();
960 * TODO: Extend to optionally specify relative/absolute url where data of this
961 * type can be retrieved from
965 public void addResultDatatype(JvDataType dt)
967 if (resultData == null)
969 resultData = new ArrayList<JvDataType>();
974 public boolean removeRsultDatatype(JvDataType dt)
976 if (resultData != null)
978 return resultData.remove(dt);
983 public List<JvDataType> getResultDataTypes()
989 * parse a concatenated list of rest service descriptions into an array
992 * @return zero or more services.
994 * if the services are improperly encoded.
996 public static List<RestServiceDescription> parseDescriptions(
997 String services) throws Exception
999 String[] list = separatorListToArray(services, "|");
1000 List<RestServiceDescription> svcparsed = new ArrayList<RestServiceDescription>();
1001 int p = 0, lastp = 0;
1002 StringBuffer warnings = new StringBuffer();
1005 RestServiceDescription rsd = new RestServiceDescription();
1006 p = rsd.parseServiceList(list, warnings, lastp = p);
1007 if (p > lastp && rsd.isValid())
1013 throw new Exception(
1014 "Failed to parse user defined RSBS services from :"
1016 + "\nFirst error was encountered at token " + lastp
1017 + " starting " + list[lastp] + ":\n"
1018 + rsd.getInvalidMessage());
1020 } while (p < lastp && p < list.length - 1);