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