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