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