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