2 * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3 * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
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/>.
18 package jalview.ws.rest;
20 import jalview.datamodel.SequenceI;
21 import jalview.io.packed.DataProvider;
22 import jalview.io.packed.SimpleDataProvider;
23 import jalview.io.packed.DataProvider.JvDataType;
24 import jalview.util.GroupUrlLink.UrlStringTooLongException;
25 import jalview.util.Platform;
26 import jalview.ws.rest.params.Alignment;
27 import jalview.ws.rest.params.AnnotationFile;
28 import jalview.ws.rest.params.JobConstant;
29 import jalview.ws.rest.params.SeqGroupIndexVector;
32 import java.util.ArrayList;
33 import java.util.HashMap;
34 import java.util.Hashtable;
35 import java.util.List;
37 import java.util.NoSuchElementException;
38 import java.util.StringTokenizer;
39 import java.util.Vector;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
43 import javax.swing.JViewport;
45 import com.stevesoft.pat.Regex;
46 import com.sun.org.apache.xml.internal.serialize.OutputFormat.DTD;
47 import com.sun.tools.doclets.internal.toolkit.util.DocFinder.Output;
49 public class RestServiceDescription
52 * create a new rest service description ready to be configured
54 public RestServiceDescription()
67 public RestServiceDescription(String action, String description,
68 String name, String postUrl, String urlSuffix,
69 Map<String, InputType> inputParams, boolean hseparable,
70 boolean vseparable, char gapCharacter)
73 this.details = new UIinfo();
74 details.Action = action == null ? "" : action;
75 details.description = description == null ? "" : description;
76 details.Name = name == null ? "" : name;
77 this.postUrl = postUrl == null ? "" : postUrl;
78 this.urlSuffix = urlSuffix == null ? "" : urlSuffix;
79 if (inputParams != null)
81 this.inputParams = inputParams;
83 this.hseparable = hseparable;
84 this.vseparable = vseparable;
85 this.gapCharacter = gapCharacter;
88 public boolean equals(Object o)
90 if (o == null || !(o instanceof RestServiceDescription))
94 RestServiceDescription other = (RestServiceDescription) o;
95 boolean diff = (gapCharacter != other.gapCharacter);
96 diff |= vseparable != other.vseparable;
97 diff |= hseparable != other.hseparable;
98 diff |= !(urlSuffix.equals(other.urlSuffix));
99 // TODO - robust diff that includes constants and reordering of URL
100 // diff |= !(postUrl.equals(other.postUrl));
101 // diff |= !inputParams.equals(other.inputParams);
102 diff |= !details.Name.equals(other.details.Name);
103 diff |= !details.Action.equals(other.details.Action);
104 diff |= !details.description.equals(other.details.description);
109 * Service UI Info { Action, Specific Name of Service, Brief Description }
114 public String getAction()
119 public void setAction(String action)
124 public String getName()
129 public void setName(String name)
134 public String getDescription()
139 public void setDescription(String description)
141 this.description = description;
151 public UIinfo details = new UIinfo();
153 public String getAction()
155 return details.getAction();
158 public void setAction(String action)
160 details.setAction(action);
163 public String getName()
165 return details.getName();
168 public void setName(String name)
170 details.setName(name);
173 public String getDescription()
175 return details.getDescription();
178 public void setDescription(String description)
180 details.setDescription(description);
188 public String getPostUrl()
193 public void setPostUrl(String postUrl)
195 this.postUrl = postUrl;
198 public String getUrlSuffix()
203 public void setUrlSuffix(String urlSuffix)
205 this.urlSuffix = urlSuffix;
208 public Map<String, InputType> getInputParams()
213 public void setInputParams(Map<String, InputType> inputParams)
215 this.inputParams = inputParams;
218 public void setHseparable(boolean hseparable)
220 this.hseparable = hseparable;
223 public void setVseparable(boolean vseparable)
225 this.vseparable = vseparable;
228 public void setGapCharacter(char gapCharacter)
230 this.gapCharacter = gapCharacter;
234 * suffix that should be added to any url used if it does not already end in
240 * input info given as key/value pairs - mapped to post arguments
242 Map<String, InputType> inputParams = new HashMap<String, InputType>();
245 * assigns the given inputType it to its corresponding input parameter token
250 public void setInputParam(InputType it)
252 inputParams.put(it.token, it);
256 * remove the given input type it from the set of service input parameters.
260 public void removeInputParam(InputType it)
262 inputParams.remove(it.token);
266 * service requests alignment data
271 * service requests alignment and/or seuqence annotationo data
276 * service requests partitions defined over input (alignment) data
278 boolean partitiondata;
281 * process ths input data and set the appropriate shorthand flags describing
282 * the input the service wants
284 public void setInvolvesFlags()
286 aligndata = inputInvolves(Alignment.class);
287 annotdata = inputInvolves(AnnotationFile.class);
288 partitiondata = inputInvolves(SeqGroupIndexVector.class);
292 * Service return info { alignment, annotation file (loaded back on to
293 * alignment), tree (loaded back on to alignment), sequence annotation -
294 * loaded back on to alignment), text report, pdb structures with sequence
300 * Start with bare minimum: input is alignment + groups on alignment
306 private String invalidMessage = null;
309 * parse the given linkString of the form '<label>|<url>|separator
310 * char[|optional sequence separator char]' into parts. url may contain a
311 * string $SEQUENCEIDS<=optional regex=>$ where <=optional regex=> must be of
312 * the form =/<perl style regex>/=$ or $SEQUENCES<=optional regex=>$ or
313 * $SEQUENCES<=optional regex=>$.
317 public RestServiceDescription(String link)
319 StringBuffer warnings = new StringBuffer();
320 if (!configureFromEncodedString(link, warnings))
322 if (warnings.length() > 0)
324 invalidMessage = warnings.toString();
329 public RestServiceDescription(RestServiceDescription toedit)
331 // Rather then do the above, we cheat and use our human readable
332 // serialization code to clone everything
333 this(toedit.toString());
335 * if (toedit == null) { return; } /** urlSuffix = toedit.urlSuffix; postUrl
336 * = toedit.postUrl; hseparable = toedit.hseparable; vseparable =
337 * toedit.vseparable; gapCharacter = toedit.gapCharacter; details = new
338 * RestServiceDescription.UIinfo(); details.Action = toedit.details.Action;
339 * details.description = toedit.details.description; details.Name =
340 * toedit.details.Name; for (InputType itype: toedit.inputParams.values()) {
341 * inputParams.put(itype.token, itype.clone());
345 // TODO Implement copy constructor NOW*/
349 * @return the invalidMessage
351 public String getInvalidMessage()
353 return invalidMessage;
357 * Check if URL string was parsed properly.
359 * @return boolean - if false then <code>getInvalidMessage</code> returns an
362 public boolean isValid()
364 return invalidMessage == null;
367 private static boolean debug = false;
370 * parse the string into a list
374 * @return elements separated by separator
376 public static String[] separatorListToArray(String list, String separator)
378 int seplen = separator.length();
379 if (list == null || list.equals("") || list.equals(separator))
381 java.util.ArrayList<String> jv = new ArrayList<String>();
382 int cp = 0, pos, escape;
383 boolean wasescaped = false,wasquoted=false;
384 String lstitem = null;
385 while ((pos = list.indexOf(separator, cp)) >= cp)
388 escape = (pos > 0 && list.charAt(pos - 1) == '\\') ? -1 : 0;
389 if (wasescaped || wasquoted)
391 // append to previous pos
392 jv.set(jv.size() - 1,
393 lstitem = lstitem + separator
394 + list.substring(cp, pos + escape));
399 jv.add(lstitem = list.substring(cp, pos + escape));
402 wasescaped = escape == -1;
405 // last separator may be in an unmatched quote
406 if (java.util.regex.Pattern.matches("('[^']*')*[^']*'",lstitem))
413 if (cp < list.length())
415 String c = list.substring(cp);
416 if (wasescaped || wasquoted)
418 // append final separator
419 jv.set(jv.size() - 1, lstitem + separator + c);
423 if (!c.equals(separator))
431 String[] v = jv.toArray(new String[jv.size()]);
435 System.err.println("Array from '" + separator
436 + "' separated List:\n" + v.length);
437 for (int i = 0; i < v.length; i++)
439 System.err.println("item " + i + " '" + v[i] + "'");
446 System.err.println("Empty Array from '" + separator
447 + "' separated List");
453 * concatenate the list with separator
457 * @return concatenated string
459 public static String arrayToSeparatorList(String[] list, String separator)
461 StringBuffer v = new StringBuffer();
462 if (list != null && list.length > 0)
464 for (int i = 0, iSize = list.length; i < iSize; i++)
472 // TODO - escape any separator values in list[i]
478 System.err.println("Returning '" + separator
479 + "' separated List:\n");
480 System.err.println(v);
486 System.err.println("Returning empty '" + separator
487 + "' separated List\n");
489 return "" + separator;
493 * parse a string containing a list of service properties and configure the
494 * service description
497 * param warnings a StringBuffer that any warnings about invalid
498 * content will be appended to.
500 private boolean configureFromServiceInputProperties(String propList,
501 StringBuffer warnings)
503 String[] props = separatorListToArray(propList, ",");
509 boolean valid = true;
511 int l = warnings.length();
513 for (String prop : props)
515 if ((i = prop.indexOf("=")) > -1)
517 val = prop.substring(i + 1);
518 if (val.startsWith("\'") && val.endsWith("\'"))
520 val = val.substring(1, val.length() - 1);
522 prop = prop.substring(0, i);
525 if (prop.equals("hseparable"))
529 if (prop.equals("vseparable"))
533 if (prop.equals("gapCharacter"))
535 if (val == null || val.length() == 0 || val.length() > 1)
538 warnings.append((warnings.length() > 0 ? "\n" : "")
539 + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
544 gapCharacter = val.charAt(0);
547 if (prop.equals("returns"))
549 _configureOutputFormatFrom(val, warnings);
552 // return true if valid is true and warning buffer was not appended to.
553 return valid && (l == warnings.length());
556 private String _genOutputFormatString()
559 if (resultData == null)
563 for (JvDataType type : resultData)
565 if (buff.length() > 0)
569 buff += type.toString();
574 private void _configureOutputFormatFrom(String outstring,
575 StringBuffer warnings)
577 if (outstring.indexOf(";") == -1)
579 // we add a token, for simplicity
580 outstring = outstring + ";";
582 StringTokenizer st = new StringTokenizer(outstring, ";");
584 resultData = new ArrayList<JvDataType>();
585 while (st.hasMoreTokens())
589 resultData.add(JvDataType.valueOf(tok = st.nextToken()));
590 } catch (NoSuchElementException x)
592 warnings.append("Invalid result type: '" + tok
593 + "' (must be one of: ");
595 for (JvDataType vl : JvDataType.values())
597 warnings.append(sep);
598 warnings.append(vl.toString());
601 warnings.append(" separated by semi-colons)\n");
606 private String getServiceIOProperties()
608 ArrayList<String> vls = new ArrayList<String>();
609 if (isHseparable()) { vls.add("hseparable");};
610 if (isVseparable()) { vls.add("vseparable");};
611 vls.add(new String("gapCharacter='" + gapCharacter + "'"));
612 vls.add(new String("returns='" + _genOutputFormatString() + "'"));
613 return arrayToSeparatorList(vls.toArray(new String[0]), ",");
616 public String toString()
618 StringBuffer result = new StringBuffer();
620 result.append(details.Name);
622 result.append(details.Action);
624 if (details.description != null)
626 result.append(details.description);
629 // list job input flags
631 result.append(getServiceIOProperties());
632 // list any additional cgi parameters needed for result retrieval
633 if (urlSuffix != null && urlSuffix.length() > 0)
636 result.append(urlSuffix);
639 result.append(getInputParamEncodedUrl());
640 return result.toString();
644 * processes a service encoded as a string (as generated by RestServiceDescription.toString())
645 * Note - this will only use the first service definition encountered in the string to configure the service.
647 * @param warnings - where warning messages are reported.
648 * @return true if configuration was parsed successfully.
650 public boolean configureFromEncodedString(String encoding,
651 StringBuffer warnings)
653 String[] list = separatorListToArray(encoding, "|");
655 int nextpos=parseServiceList(list,warnings, 0);
663 * processes the given list from position p, attempting to configure the service from it.
664 * Service lists are formed by concatenating individual stringified services. The first character of a stringified service is '|', enabling this, and the parser will ignore empty fields in a '|' separated list when they fall outside a service definition.
670 protected int parseServiceList(String[] list, StringBuffer warnings, int p)
672 boolean invalid = false;
673 // look for the first non-empty position - expect it to be service name
674 while (list[p]!=null && list[p].trim().length()==0)
678 details.Name = list[p];
679 details.Action = list[p+1];
680 details.description = list[p+2];
681 invalid |= !configureFromServiceInputProperties(list[p+3], warnings);
682 if (list.length-p > 5 && list[p+5]!=null && list[p+5].trim().length()>5)
684 urlSuffix = list[p+4];
685 invalid |= !configureFromInputParamEncodedUrl(list[p+5], warnings);
690 if (list.length-p > 4 && list[p+4]!=null && list[p+4].trim().length()>5)
693 invalid |= !configureFromInputParamEncodedUrl(list[p+4], warnings);
697 return invalid ? -1 : p;
701 * @return string representation of the input parameters, their type and
702 * constraints, appended to the service's base submission URL
704 private String getInputParamEncodedUrl()
706 StringBuffer url = new StringBuffer();
707 if (postUrl == null || postUrl.length() < 5)
713 char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
714 boolean consts = true;
717 for (Map.Entry<String, InputType> param : inputParams.entrySet())
719 List<String> vals = param.getValue().getURLEncodedParameter();
720 if (param.getValue().isConstant())
724 url.append(appendChar);
726 url.append(param.getValue().token);
727 if (vals.size() == 1)
730 url.append(vals.get(0));
738 url.append(appendChar);
740 url.append(param.getValue().token);
742 // write parameter set as $TOKENPREFIX:csv list of params$ for this
745 url.append(param.getValue().getURLtokenPrefix());
747 url.append(arrayToSeparatorList(vals.toArray(new String[0]),
754 // toggle consts and repeat until !consts is false:
755 } while (!(consts = !consts));
756 return url.toString();
760 * parse the service URL and input parameters from the given encoded URL
761 * string and configure the RestServiceDescription from it.
766 * @return true if URL parsed correctly. false means the configuration failed.
768 private boolean configureFromInputParamEncodedUrl(String ipurl,
769 StringBuffer warnings)
771 boolean valid = true;
773 String url = new String();
774 Matcher prms = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$")
776 Map<String, InputType> iparams = new Hashtable<String, InputType>();
780 if (lastp < prms.start(0))
782 url += ipurl.substring(lastp, prms.start(0));
783 lastp = prms.end(0) + 1;
785 String sep = prms.group(1);
786 String tok = prms.group(2);
787 String iprm = prms.group(3);
788 int colon = iprm.indexOf(":");
789 String iprmparams = "";
792 iprmparams = iprm.substring(colon + 1);
793 iprm = iprm.substring(0, colon);
795 valid = parseTypeString(prms.group(0), tok, iprm, iprmparams,
802 URL u = new URL(url);
804 inputParams = iparams;
805 } catch (Exception e)
807 warnings.append("Failed to parse '" + url + "' as a URL.\n");
814 public static Class[] getInputTypes()
816 // TODO - find a better way of maintaining this classlist
818 { jalview.ws.rest.params.Alignment.class,
819 jalview.ws.rest.params.AnnotationFile.class,
820 SeqGroupIndexVector.class,
821 jalview.ws.rest.params.SeqIdVector.class,
822 jalview.ws.rest.params.SeqVector.class,
823 jalview.ws.rest.params.Tree.class };
826 public static boolean parseTypeString(String fullstring, String tok,
827 String iprm, String iprmparams, Map<String, InputType> iparams,
828 StringBuffer warnings)
830 boolean valid = true;
832 for (Class type : getInputTypes())
836 jinput = (InputType) (type.getConstructor().newInstance(null));
837 if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
839 ArrayList<String> al = new ArrayList<String>();
840 for (String prprm : separatorListToArray(iprmparams, ","))
842 // hack to ensure that strings like "sep=','" containing unescaped commas as values are concatenated
843 al.add(prprm.trim());
845 if (!jinput.configureFromURLtokenString(al, warnings))
848 warnings.append("Failed to parse '" + fullstring + "' as a "
849 + jinput.getURLtokenPrefix() + " input tag.\n");
854 iparams.put(tok, jinput);
860 } catch (Throwable thr)
868 public static void main(String argv[])
870 // test separator list
872 assert(separatorListToArray("foo=',',min='foo',max='1,2,3',fa=','", ",").length==4);
873 if (separatorListToArray("minsize='2', sep=','", ",").length==2)
878 } catch (AssertionError x)
880 System.err.println("separatorListToArray is faulty.");
882 if (argv.length == 0)
884 if (!testRsdExchange("Test using default Shmmr service",
885 RestClient.makeShmmrRestClient().service))
887 System.err.println("default test failed.");
891 System.err.println("default test passed.");
897 for (String svc : argv)
899 p += testRsdExchange("Test " + (++i), svc) ? 1 : 0;
901 System.err.println("" + p + " out of " + i + " tests passed.");
906 private static boolean testRsdExchange(String desc, String servicestring)
910 RestServiceDescription newService = new RestServiceDescription(
912 if (!newService.isValid())
914 throw new Error("Failed to create service from '" + servicestring
915 + "'.\n" + newService.getInvalidMessage());
917 return testRsdExchange(desc, newService);
918 } catch (Throwable x)
920 System.err.println("Failed for service (" + desc + "): "
927 private static boolean testRsdExchange(String desc,
928 RestServiceDescription service)
932 String fromservicetostring = service.toString();
933 RestServiceDescription newService = new RestServiceDescription(
934 fromservicetostring);
935 if (!newService.isValid())
937 throw new Error("Failed to create service from '"
938 + fromservicetostring + "'.\n"
939 + newService.getInvalidMessage());
942 if (!service.equals(newService))
944 System.err.println("Failed for service (" + desc + ").");
945 System.err.println("Original service and parsed service differ.");
946 System.err.println("Original: " + fromservicetostring);
947 System.err.println("Parsed : " + newService.toString());
950 } catch (Throwable x)
952 System.err.println("Failed for service (" + desc + "): "
953 + service.toString());
961 * covenience method to generate the id and sequence string vector from a set
962 * of seuqences using each sequence's getName() and getSequenceAsString()
966 * @return String[][] {{sequence ids},{sequence strings}}
968 public static String[][] formStrings(SequenceI[] seqs)
970 String[][] idset = new String[2][seqs.length];
971 for (int i = 0; i < seqs.length; i++)
973 idset[0][i] = seqs[i].getName();
974 idset[1][i] = seqs[i].getSequenceAsString();
980 * can this service be run on the visible portion of an alignment regardless
981 * of hidden boundaries ?
983 boolean hseparable = false;
985 boolean vseparable = false;
987 public boolean isHseparable()
996 public boolean isVseparable()
1002 * search the input types for an instance of the given class
1004 * @param <validInput.inputType> class1
1007 public boolean inputInvolves(Class<?> class1)
1009 assert (InputType.class.isAssignableFrom(class1));
1010 for (InputType val : inputParams.values())
1012 if (class1.isAssignableFrom(val.getClass()))
1020 char gapCharacter = '-';
1024 * @return the preferred gap character for alignments input/output by this
1027 public char getGapCharacter()
1029 return gapCharacter;
1032 public String getDecoratedResultUrl(String jobId)
1034 // TODO: correctly write ?/& appropriate to result URL format.
1035 return jobId + urlSuffix;
1038 private List<JvDataType> resultData = new ArrayList<JvDataType>();
1043 * TODO: Extend to optionally specify relative/absolute url where data of this
1044 * type can be retrieved from
1048 public void addResultDatatype(JvDataType dt)
1050 if (resultData == null)
1052 resultData = new ArrayList<JvDataType>();
1057 public boolean removeRsultDatatype(JvDataType dt)
1059 if (resultData != null)
1061 return resultData.remove(dt);
1066 public List<JvDataType> getResultDataTypes()
1072 * parse a concatenated list of rest service descriptions into an array
1074 * @return zero or more services.
1075 * @throws exceptions if the services are improperly encoded.
1077 public static List<RestServiceDescription> parseDescriptions(String services) throws Exception
1079 String[] list = separatorListToArray(services, "|");
1080 List<RestServiceDescription> svcparsed = new ArrayList<RestServiceDescription>();
1082 StringBuffer warnings=new StringBuffer();
1084 RestServiceDescription rsd = new RestServiceDescription();
1085 p=rsd.parseServiceList(list, warnings, lastp=p);
1086 if (p>lastp && rsd.isValid())
1090 throw new Exception("Failed to parse user defined RSBS services from :"+services+"\nFirst error was encountered at token "+lastp+" starting "+list[lastp]+":\n"+rsd.getInvalidMessage());
1092 } while (p<lastp && p<list.length-1);