JAL-715 - remove platform dependent includes
[jalview.git] / src / jalview / ws / rest / RestServiceDescription.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, G Barton, M Clamp, S Searle
4  * 
5  * This file is part of Jalview.
6  * 
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.
10  * 
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.
15  * 
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  */
18 package jalview.ws.rest;
19
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;
30
31 import java.net.URL;
32 import java.util.ArrayList;
33 import java.util.HashMap;
34 import java.util.Hashtable;
35 import java.util.List;
36 import java.util.Map;
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;
42
43 import javax.swing.JViewport;
44
45 import com.stevesoft.pat.Regex;
46 import com.sun.org.apache.xml.internal.serialize.OutputFormat.DTD;
47
48 public class RestServiceDescription
49 {
50   /**
51    * create a new rest service description ready to be configured
52    */
53   public RestServiceDescription()
54   {
55     
56   }
57   /**
58    * @param details
59    * @param postUrl
60    * @param urlSuffix
61    * @param inputParams
62    * @param hseparable
63    * @param vseparable
64    * @param gapCharacter
65    */
66   public RestServiceDescription(String action, String description,
67           String name, String postUrl, String urlSuffix,
68           Map<String, InputType> inputParams, boolean hseparable,
69           boolean vseparable, char gapCharacter)
70   {
71     super();
72     this.details = new UIinfo();
73     details.Action = action == null ? "" : action;
74     details.description = description == null ? "" : description;
75     details.Name = name == null ? "" : name;
76     this.postUrl = postUrl == null ? "" : postUrl;
77     this.urlSuffix = urlSuffix == null ? "" : urlSuffix;
78     if (inputParams != null)
79     {
80       this.inputParams = inputParams;
81     }
82     this.hseparable = hseparable;
83     this.vseparable = vseparable;
84     this.gapCharacter = gapCharacter;
85   }
86
87   public boolean equals(Object o)
88   {
89     if (o == null || !(o instanceof RestServiceDescription))
90     {
91       return false;
92     }
93     RestServiceDescription other = (RestServiceDescription) o;
94     boolean diff = (gapCharacter != other.gapCharacter);
95     diff |= vseparable != other.vseparable;
96     diff |= hseparable != other.hseparable;
97     diff |= !(urlSuffix.equals(other.urlSuffix));
98     // TODO - robust diff that includes constants and reordering of URL
99     // diff |= !(postUrl.equals(other.postUrl));
100     // diff |= !inputParams.equals(other.inputParams);
101     diff |= !details.Name.equals(other.details.Name);
102     diff |= !details.Action.equals(other.details.Action);
103     diff |= !details.description.equals(other.details.description);
104     return !diff;
105   }
106
107   /**
108    * Service UI Info { Action, Specific Name of Service, Brief Description }
109    */
110
111   public class UIinfo
112   {
113     public String getAction()
114     {
115       return Action;
116     }
117
118     public void setAction(String action)
119     {
120       Action = action;
121     }
122
123     public String getName()
124     {
125       return Name;
126     }
127
128     public void setName(String name)
129     {
130       Name = name;
131     }
132
133     public String getDescription()
134     {
135       return description;
136     }
137
138     public void setDescription(String description)
139     {
140       this.description = description;
141     }
142
143     String Action;
144
145     String Name;
146
147     String description;
148   }
149
150   public UIinfo details = new UIinfo();
151
152   public String getAction()
153   {
154     return details.getAction();
155   }
156
157   public void setAction(String action)
158   {
159     details.setAction(action);
160   }
161
162   public String getName()
163   {
164     return details.getName();
165   }
166
167   public void setName(String name)
168   {
169     details.setName(name);
170   }
171
172   public String getDescription()
173   {
174     return details.getDescription();
175   }
176
177   public void setDescription(String description)
178   {
179     details.setDescription(description);
180   }
181
182   /**
183    * Service base URL
184    */
185   String postUrl;
186
187   public String getPostUrl()
188   {
189     return postUrl;
190   }
191
192   public void setPostUrl(String postUrl)
193   {
194     this.postUrl = postUrl;
195   }
196
197   public String getUrlSuffix()
198   {
199     return urlSuffix;
200   }
201
202   public void setUrlSuffix(String urlSuffix)
203   {
204     this.urlSuffix = urlSuffix;
205   }
206
207   public Map<String, InputType> getInputParams()
208   {
209     return inputParams;
210   }
211
212   public void setInputParams(Map<String, InputType> inputParams)
213   {
214     this.inputParams = inputParams;
215   }
216
217   public void setHseparable(boolean hseparable)
218   {
219     this.hseparable = hseparable;
220   }
221
222   public void setVseparable(boolean vseparable)
223   {
224     this.vseparable = vseparable;
225   }
226
227   public void setGapCharacter(char gapCharacter)
228   {
229     this.gapCharacter = gapCharacter;
230   }
231
232   /**
233    * suffix that should be added to any url used if it does not already end in
234    * the suffix.
235    */
236   String urlSuffix;
237
238   /**
239    * input info given as key/value pairs - mapped to post arguments
240    */
241   Map<String, InputType> inputParams = new HashMap<String, InputType>();
242
243   /**
244    * assigns the given inputType it to its corresponding input parameter token
245    * it.token
246    * 
247    * @param it
248    */
249   public void setInputParam(InputType it)
250   {
251     inputParams.put(it.token, it);
252   }
253
254   /**
255    * remove the given input type it from the set of service input parameters.
256    * 
257    * @param it
258    */
259   public void removeInputParam(InputType it)
260   {
261     inputParams.remove(it.token);
262   }
263
264   /**
265    * service requests alignment data
266    */
267   boolean aligndata;
268
269   /**
270    * service requests alignment and/or seuqence annotationo data
271    */
272   boolean annotdata;
273
274   /**
275    * service requests partitions defined over input (alignment) data
276    */
277   boolean partitiondata;
278
279   /**
280    * process ths input data and set the appropriate shorthand flags describing
281    * the input the service wants
282    */
283   public void setInvolvesFlags()
284   {
285     aligndata = inputInvolves(Alignment.class);
286     annotdata = inputInvolves(AnnotationFile.class);
287     partitiondata = inputInvolves(SeqGroupIndexVector.class);
288   }
289
290   /**
291    * Service return info { alignment, annotation file (loaded back on to
292    * alignment), tree (loaded back on to alignment), sequence annotation -
293    * loaded back on to alignment), text report, pdb structures with sequence
294    * mapping )
295    * 
296    */
297
298   /**
299    * Start with bare minimum: input is alignment + groups on alignment
300    * 
301    * @author JimP
302    * 
303    */
304
305   private String invalidMessage = null;
306
307   /**
308    * parse the given linkString of the form '<label>|<url>|separator
309    * char[|optional sequence separator char]' into parts. url may contain a
310    * string $SEQUENCEIDS<=optional regex=>$ where <=optional regex=> must be of
311    * the form =/<perl style regex>/=$ or $SEQUENCES<=optional regex=>$ or
312    * $SEQUENCES<=optional regex=>$.
313    * 
314    * @param link
315    */
316   public RestServiceDescription(String link)
317   {
318     StringBuffer warnings = new StringBuffer();
319     if (!configureFromEncodedString(link, warnings))
320     {
321       if (warnings.length() > 0)
322       {
323         invalidMessage = warnings.toString();
324       }
325     }
326   }
327
328   public RestServiceDescription(RestServiceDescription toedit)
329   {
330     // Rather then do the above, we cheat and use our human readable
331     // serialization code to clone everything
332     this(toedit.toString());
333     /**
334      * if (toedit == null) { return; } /** urlSuffix = toedit.urlSuffix; postUrl
335      * = toedit.postUrl; hseparable = toedit.hseparable; vseparable =
336      * toedit.vseparable; gapCharacter = toedit.gapCharacter; details = new
337      * RestServiceDescription.UIinfo(); details.Action = toedit.details.Action;
338      * details.description = toedit.details.description; details.Name =
339      * toedit.details.Name; for (InputType itype: toedit.inputParams.values()) {
340      * inputParams.put(itype.token, itype.clone());
341      * 
342      * }
343      */
344     // TODO Implement copy constructor NOW*/
345   }
346
347   /**
348    * @return the invalidMessage
349    */
350   public String getInvalidMessage()
351   {
352     return invalidMessage;
353   }
354
355   /**
356    * Check if URL string was parsed properly.
357    * 
358    * @return boolean - if false then <code>getInvalidMessage</code> returns an
359    *         error message
360    */
361   public boolean isValid()
362   {
363     return invalidMessage == null;
364   }
365
366   private static boolean debug = false;
367
368   /**
369    * parse the string into a list
370    * 
371    * @param list
372    * @param separator
373    * @return elements separated by separator
374    */
375   public static String[] separatorListToArray(String list, String separator)
376   {
377     int seplen = separator.length();
378     if (list == null || list.equals("") || list.equals(separator))
379       return null;
380     java.util.ArrayList<String> jv = new ArrayList<String>();
381     int cp = 0, pos, escape;
382     boolean wasescaped = false,wasquoted=false;
383     String lstitem = null;
384     while ((pos = list.indexOf(separator, cp)) >= cp)
385     {
386       
387       escape = (pos > 0 && list.charAt(pos - 1) == '\\') ? -1 : 0;
388       if (wasescaped || wasquoted)
389       {
390         // append to previous pos
391         jv.set(jv.size() - 1,
392                 lstitem = lstitem + separator
393                         + list.substring(cp, pos + escape));
394
395       }
396       else
397       {
398         jv.add(lstitem = list.substring(cp, pos + escape));
399       }
400       cp = pos + seplen;
401       wasescaped = escape == -1;
402       if (!wasescaped)
403       {
404         // last separator may be in an unmatched quote
405         if (java.util.regex.Pattern.matches("('[^']*')*[^']*'",lstitem))
406         {
407           wasquoted=true;
408         }
409       }
410       
411     }
412     if (cp < list.length())
413     {
414       String c = list.substring(cp);
415       if (wasescaped || wasquoted)
416       {
417         // append final separator
418         jv.set(jv.size() - 1, lstitem + separator + c);
419       }
420       else
421       {
422         if (!c.equals(separator))
423         {
424           jv.add(c);
425         }
426       }
427     }
428     if (jv.size() > 0)
429     {
430       String[] v = jv.toArray(new String[jv.size()]);
431       jv.clear();
432       if (debug)
433       {
434         System.err.println("Array from '" + separator
435                 + "' separated List:\n" + v.length);
436         for (int i = 0; i < v.length; i++)
437         {
438           System.err.println("item " + i + " '" + v[i] + "'");
439         }
440       }
441       return v;
442     }
443     if (debug)
444     {
445       System.err.println("Empty Array from '" + separator
446               + "' separated List");
447     }
448     return null;
449   }
450
451   /**
452    * concatenate the list with separator
453    * 
454    * @param list
455    * @param separator
456    * @return concatenated string
457    */
458   public static String arrayToSeparatorList(String[] list, String separator)
459   {
460     StringBuffer v = new StringBuffer();
461     if (list != null && list.length > 0)
462     {
463       for (int i = 0, iSize = list.length; i < iSize; i++)
464       {
465         if (list[i] != null)
466         {
467           if (v.length() > 0)
468           {
469             v.append(separator);
470           }
471           // TODO - escape any separator values in list[i]
472           v.append(list[i]);
473         }
474       }
475       if (debug)
476       {
477         System.err.println("Returning '" + separator
478                 + "' separated List:\n");
479         System.err.println(v);
480       }
481       return v.toString();
482     }
483     if (debug)
484     {
485       System.err.println("Returning empty '" + separator
486               + "' separated List\n");
487     }
488     return "" + separator;
489   }
490
491   /**
492    * parse a string containing a list of service properties and configure the
493    * service description
494    * 
495    * @param propList
496    *          param warnings a StringBuffer that any warnings about invalid
497    *          content will be appended to.
498    */
499   private boolean configureFromServiceInputProperties(String propList,
500           StringBuffer warnings)
501   {
502     String[] props = separatorListToArray(propList, ",");
503     if (props == null)
504     {
505       return true;
506     }
507     ;
508     boolean valid = true;
509     String val = null;
510     int l = warnings.length();
511     int i;
512     for (String prop : props)
513     {
514       if ((i = prop.indexOf("=")) > -1)
515       {
516         val = prop.substring(i + 1);
517         if (val.startsWith("\'") && val.endsWith("\'"))
518         {
519           val = val.substring(1, val.length() - 1);
520         }
521         prop = prop.substring(0, i);
522       }
523
524       if (prop.equals("hseparable"))
525       {
526         hseparable = true;
527       }
528       if (prop.equals("vseparable"))
529       {
530         vseparable = true;
531       }
532       if (prop.equals("gapCharacter"))
533       {
534         if (val == null || val.length() == 0 || val.length() > 1)
535         {
536           valid = false;
537           warnings.append((warnings.length() > 0 ? "\n" : "")
538                   + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
539                           + val + "'"));
540         }
541         else
542         {
543           gapCharacter = val.charAt(0);
544         }
545       }
546       if (prop.equals("returns"))
547       {
548         _configureOutputFormatFrom(val, warnings);
549       }
550     }
551     // return true if valid is true and warning buffer was not appended to.
552     return valid && (l == warnings.length());
553   }
554
555   private String _genOutputFormatString()
556   {
557     String buff = "";
558     if (resultData == null)
559     {
560       return "";
561     }
562     for (JvDataType type : resultData)
563     {
564       if (buff.length() > 0)
565       {
566         buff += ";";
567       }
568       buff += type.toString();
569     }
570     return buff;
571   }
572
573   private void _configureOutputFormatFrom(String outstring,
574           StringBuffer warnings)
575   {
576     if (outstring.indexOf(";") == -1)
577     {
578       // we add a token, for simplicity
579       outstring = outstring + ";";
580     }
581     StringTokenizer st = new StringTokenizer(outstring, ";");
582     String tok = "";
583     resultData = new ArrayList<JvDataType>();
584     while (st.hasMoreTokens())
585     {
586       try
587       {
588         resultData.add(JvDataType.valueOf(tok = st.nextToken()));
589       } catch (NoSuchElementException x)
590       {
591         warnings.append("Invalid result type: '" + tok
592                 + "' (must be one of: ");
593         String sep = "";
594         for (JvDataType vl : JvDataType.values())
595         {
596           warnings.append(sep);
597           warnings.append(vl.toString());
598           sep = " ,";
599         }
600         warnings.append(" separated by semi-colons)\n");
601       }
602     }
603   }
604
605   private String getServiceIOProperties()
606   {
607     ArrayList<String> vls = new ArrayList<String>();
608     if (isHseparable()) { vls.add("hseparable");};
609     if (isVseparable()) { vls.add("vseparable");};
610     vls.add(new String("gapCharacter='" + gapCharacter + "'"));
611     vls.add(new String("returns='" + _genOutputFormatString() + "'"));
612     return arrayToSeparatorList(vls.toArray(new String[0]), ",");
613   }
614
615   public String toString()
616   {
617     StringBuffer result = new StringBuffer();
618     result.append("|");
619     result.append(details.Name);
620     result.append('|');
621     result.append(details.Action);
622     result.append('|');
623     if (details.description != null)
624     {
625       result.append(details.description);
626     }
627     ;
628     // list job input flags
629     result.append('|');
630     result.append(getServiceIOProperties());
631     // list any additional cgi parameters needed for result retrieval
632     if (urlSuffix != null && urlSuffix.length() > 0)
633     {
634       result.append('|');
635       result.append(urlSuffix);
636     }
637     result.append('|');
638     result.append(getInputParamEncodedUrl());
639     return result.toString();
640   }
641
642   /**
643    * processes a service encoded as a string (as generated by RestServiceDescription.toString())
644    * Note - this will only use the first service definition encountered in the string to configure the service.
645    * @param encoding
646    * @param warnings - where warning messages are reported.
647    * @return true if configuration was parsed successfully. 
648    */
649   public boolean configureFromEncodedString(String encoding,
650           StringBuffer warnings)
651   {
652     String[] list = separatorListToArray(encoding, "|");
653     
654     int nextpos=parseServiceList(list,warnings, 0);
655     if (nextpos>0)
656     {
657       return true;
658     }
659     return false;
660   }
661   /**
662    * processes the given list from position p, attempting to configure the service from it.
663    * 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.
664    * @param list
665    * @param warnings
666    * @param p
667    * @return
668    */
669   protected int parseServiceList(String[] list, StringBuffer warnings, int p)
670   {
671     boolean invalid = false;
672     // look for the first non-empty position - expect it to be service name
673     while (list[p]!=null && list[p].trim().length()==0)
674     {
675       p++;
676     }
677     details.Name = list[p];
678     details.Action = list[p+1];
679     details.description = list[p+2];
680     invalid |= !configureFromServiceInputProperties(list[p+3], warnings);
681     if (list.length-p > 5 && list[p+5]!=null && list[p+5].trim().length()>5)
682     {
683       urlSuffix = list[p+4];
684       invalid |= !configureFromInputParamEncodedUrl(list[p+5], warnings);
685       p+=6;
686     }
687     else
688     {
689       if (list.length-p > 4 && list[p+4]!=null && list[p+4].trim().length()>5)
690       {
691         urlSuffix = null;
692         invalid |= !configureFromInputParamEncodedUrl(list[p+4], warnings);
693         p+=5;
694       }
695     }
696     return invalid ? -1 : p;
697   }
698   
699   /**
700    * @return string representation of the input parameters, their type and
701    *         constraints, appended to the service's base submission URL
702    */
703   private String getInputParamEncodedUrl()
704   {
705     StringBuffer url = new StringBuffer();
706     if (postUrl == null || postUrl.length() < 5)
707     {
708       return "";
709     }
710
711     url.append(postUrl);
712     char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
713     boolean consts = true;
714     do
715     {
716       for (Map.Entry<String, InputType> param : inputParams.entrySet())
717       {
718         List<String> vals = param.getValue().getURLEncodedParameter();
719         if (param.getValue().isConstant())
720         {
721           if (consts)
722           {
723             url.append(appendChar);
724             appendChar = '&';
725             url.append(param.getValue().token);
726             if (vals.size() == 1)
727             {
728               url.append("=");
729               url.append(vals.get(0));
730             }
731           }
732         }
733         else
734         {
735           if (!consts)
736           {
737             url.append(appendChar);
738             appendChar = '&';
739             url.append(param.getValue().token);
740             url.append("=");
741             // write parameter set as $TOKENPREFIX:csv list of params$ for this
742             // input param
743             url.append("$");
744             url.append(param.getValue().getURLtokenPrefix());
745             url.append(":");
746             url.append(arrayToSeparatorList(vals.toArray(new String[0]),
747                     ","));
748             url.append("$");
749           }
750         }
751
752       }
753       // toggle consts and repeat until !consts is false:
754     } while (!(consts = !consts));
755     return url.toString();
756   }
757
758   /**
759    * parse the service URL and input parameters from the given encoded URL
760    * string and configure the RestServiceDescription from it.
761    * 
762    * @param ipurl
763    * @param warnings
764    *          where any warnings
765    * @return true if URL parsed correctly. false means the configuration failed.
766    */
767   private boolean configureFromInputParamEncodedUrl(String ipurl,
768           StringBuffer warnings)
769   {
770     boolean valid = true;
771     int lastp = 0;
772     String url = new String();
773     Matcher prms = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$")
774             .matcher(ipurl);
775     Map<String, InputType> iparams = new Hashtable<String, InputType>();
776     InputType jinput;
777     while (prms.find())
778     {
779       if (lastp < prms.start(0))
780       {
781         url += ipurl.substring(lastp, prms.start(0));
782         lastp = prms.end(0) + 1;
783       }
784       String sep = prms.group(1);
785       String tok = prms.group(2);
786       String iprm = prms.group(3);
787       int colon = iprm.indexOf(":");
788       String iprmparams = "";
789       if (colon > -1)
790       {
791         iprmparams = iprm.substring(colon + 1);
792         iprm = iprm.substring(0, colon);
793       }
794       valid = parseTypeString(prms.group(0), tok, iprm, iprmparams,
795               iparams, warnings);
796     }
797     if (valid)
798     {
799       try
800       {
801         URL u = new URL(url);
802         postUrl = url;
803         inputParams = iparams;
804       } catch (Exception e)
805       {
806         warnings.append("Failed to parse '" + url + "' as a URL.\n");
807         valid = false;
808       }
809     }
810     return valid;
811   }
812
813   public static Class[] getInputTypes()
814   {
815     // TODO - find a better way of maintaining this classlist
816     return new Class[]
817     { jalview.ws.rest.params.Alignment.class,
818         jalview.ws.rest.params.AnnotationFile.class,
819         SeqGroupIndexVector.class,
820         jalview.ws.rest.params.SeqIdVector.class,
821         jalview.ws.rest.params.SeqVector.class,
822         jalview.ws.rest.params.Tree.class };
823   }
824
825   public static boolean parseTypeString(String fullstring, String tok,
826           String iprm, String iprmparams, Map<String, InputType> iparams,
827           StringBuffer warnings)
828   {
829     boolean valid = true;
830     InputType jinput;
831     for (Class type : getInputTypes())
832     {
833       try
834       {
835         jinput = (InputType) (type.getConstructor().newInstance(null));
836         if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
837         {
838           ArrayList<String> al = new ArrayList<String>();
839           for (String prprm : separatorListToArray(iprmparams, ","))
840           {
841             // hack to ensure that strings like "sep=','" containing unescaped commas as values are concatenated
842             al.add(prprm.trim());
843           }
844           if (!jinput.configureFromURLtokenString(al, warnings))
845           {
846             valid = false;
847             warnings.append("Failed to parse '" + fullstring + "' as a "
848                     + jinput.getURLtokenPrefix() + " input tag.\n");
849           }
850           else
851           {
852             jinput.token = tok;
853             iparams.put(tok, jinput);
854             valid = true;
855           }
856           break;
857         }
858
859       } catch (Throwable thr)
860       {
861       }
862       ;
863     }
864     return valid;
865   }
866
867   public static void main(String argv[])
868   {
869     // test separator list
870     try {
871       assert(separatorListToArray("foo=',',min='foo',max='1,2,3',fa=','", ",").length==4);
872       if (separatorListToArray("minsize='2', sep=','", ",").length==2)
873       {
874         assert(false);
875       }
876       
877     } catch (AssertionError x)
878     {
879       System.err.println("separatorListToArray is faulty.");
880     }
881     if (argv.length == 0)
882     {
883       if (!testRsdExchange("Test using default Shmmr service",
884               RestClient.makeShmmrRestClient().service))
885       {
886         System.err.println("default test failed.");
887       }
888       else
889       {
890         System.err.println("default test passed.");
891       }
892     }
893     else
894     {
895       int i = 0, p = 0;
896       for (String svc : argv)
897       {
898         p += testRsdExchange("Test " + (++i), svc) ? 1 : 0;
899       }
900       System.err.println("" + p + " out of " + i + " tests passed.");
901
902     }
903   }
904
905   private static boolean testRsdExchange(String desc, String servicestring)
906   {
907     try
908     {
909       RestServiceDescription newService = new RestServiceDescription(
910               servicestring);
911       if (!newService.isValid())
912       {
913         throw new Error("Failed to create service from '" + servicestring
914                 + "'.\n" + newService.getInvalidMessage());
915       }
916       return testRsdExchange(desc, newService);
917     } catch (Throwable x)
918     {
919       System.err.println("Failed for service (" + desc + "): "
920               + servicestring);
921       x.printStackTrace();
922       return false;
923     }
924   }
925
926   private static boolean testRsdExchange(String desc,
927           RestServiceDescription service)
928   {
929     try
930     {
931       String fromservicetostring = service.toString();
932       RestServiceDescription newService = new RestServiceDescription(
933               fromservicetostring);
934       if (!newService.isValid())
935       {
936         throw new Error("Failed to create service from '"
937                 + fromservicetostring + "'.\n"
938                 + newService.getInvalidMessage());
939       }
940
941       if (!service.equals(newService))
942       {
943         System.err.println("Failed for service (" + desc + ").");
944         System.err.println("Original service and parsed service differ.");
945         System.err.println("Original: " + fromservicetostring);
946         System.err.println("Parsed  : " + newService.toString());
947         return false;
948       }
949     } catch (Throwable x)
950     {
951       System.err.println("Failed for service (" + desc + "): "
952               + service.toString());
953       x.printStackTrace();
954       return false;
955     }
956     return true;
957   }
958
959   /**
960    * covenience method to generate the id and sequence string vector from a set
961    * of seuqences using each sequence's getName() and getSequenceAsString()
962    * method
963    * 
964    * @param seqs
965    * @return String[][] {{sequence ids},{sequence strings}}
966    */
967   public static String[][] formStrings(SequenceI[] seqs)
968   {
969     String[][] idset = new String[2][seqs.length];
970     for (int i = 0; i < seqs.length; i++)
971     {
972       idset[0][i] = seqs[i].getName();
973       idset[1][i] = seqs[i].getSequenceAsString();
974     }
975     return idset;
976   }
977
978   /**
979    * can this service be run on the visible portion of an alignment regardless
980    * of hidden boundaries ?
981    */
982   boolean hseparable = false;
983
984   boolean vseparable = false;
985
986   public boolean isHseparable()
987   {
988     return hseparable;
989   }
990
991   /**
992    * 
993    * @return
994    */
995   public boolean isVseparable()
996   {
997     return vseparable;
998   }
999
1000   /**
1001    * search the input types for an instance of the given class
1002    * 
1003    * @param <validInput.inputType> class1
1004    * @return
1005    */
1006   public boolean inputInvolves(Class<?> class1)
1007   {
1008     assert (InputType.class.isAssignableFrom(class1));
1009     for (InputType val : inputParams.values())
1010     {
1011       if (class1.isAssignableFrom(val.getClass()))
1012       {
1013         return true;
1014       }
1015     }
1016     return false;
1017   }
1018
1019   char gapCharacter = '-';
1020
1021   /**
1022    * 
1023    * @return the preferred gap character for alignments input/output by this
1024    *         service
1025    */
1026   public char getGapCharacter()
1027   {
1028     return gapCharacter;
1029   }
1030
1031   public String getDecoratedResultUrl(String jobId)
1032   {
1033     // TODO: correctly write ?/& appropriate to result URL format.
1034     return jobId + urlSuffix;
1035   }
1036
1037   private List<JvDataType> resultData = new ArrayList<JvDataType>();
1038
1039   /**
1040    * 
1041    * 
1042    * TODO: Extend to optionally specify relative/absolute url where data of this
1043    * type can be retrieved from
1044    * 
1045    * @param dt
1046    */
1047   public void addResultDatatype(JvDataType dt)
1048   {
1049     if (resultData == null)
1050     {
1051       resultData = new ArrayList<JvDataType>();
1052     }
1053     resultData.add(dt);
1054   }
1055
1056   public boolean removeRsultDatatype(JvDataType dt)
1057   {
1058     if (resultData != null)
1059     {
1060       return resultData.remove(dt);
1061     }
1062     return false;
1063   }
1064
1065   public List<JvDataType> getResultDataTypes()
1066   {
1067     return resultData;
1068   }
1069
1070   /**
1071    * parse a concatenated list of rest service descriptions into an array
1072    * @param services
1073    * @return zero or more services.
1074    * @throws exceptions if the services are improperly encoded.
1075    */
1076   public static List<RestServiceDescription> parseDescriptions(String services) throws Exception
1077   {
1078     String[] list = separatorListToArray(services, "|");
1079     List<RestServiceDescription> svcparsed = new ArrayList<RestServiceDescription>();
1080     int p=0,lastp=0;
1081     StringBuffer warnings=new StringBuffer();
1082     do {
1083       RestServiceDescription rsd = new RestServiceDescription();
1084       p=rsd.parseServiceList(list, warnings, lastp=p);
1085       if (p>lastp && rsd.isValid())
1086       {
1087         svcparsed.add(rsd);
1088       } else {
1089         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());
1090       }
1091     } while (p<lastp && p<list.length-1);
1092     return svcparsed;
1093   }
1094
1095 }