JAL-919, JAL-715 - prototype service editing dialog using jalview parameter model...
[jalview.git] / src / jalview / ws / rest / RestServiceDescription.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.6)
3  * Copyright (C) 2010 J Procter, AM Waterhouse, G Barton, M Clamp, S Searle
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
10  * 
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.ws.rest;
19
20 import jalview.datamodel.SequenceI;
21 import jalview.io.packed.DataProvider;
22 import jalview.io.packed.SimpleDataProvider;
23 import jalview.io.packed.DataProvider.JvDataType;
24 import jalview.util.GroupUrlLink.UrlStringTooLongException;
25 import jalview.util.Platform;
26 import jalview.ws.rest.params.Alignment;
27 import jalview.ws.rest.params.AnnotationFile;
28 import jalview.ws.rest.params.JobConstant;
29 import jalview.ws.rest.params.SeqGroupIndexVector;
30
31 import java.net.URL;
32 import java.util.ArrayList;
33 import java.util.HashMap;
34 import java.util.Hashtable;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.NoSuchElementException;
38 import java.util.StringTokenizer;
39 import java.util.Vector;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42
43 import javax.swing.JViewport;
44
45 import com.stevesoft.pat.Regex;
46 import com.sun.org.apache.xml.internal.serialize.OutputFormat.DTD;
47 import com.sun.tools.doclets.internal.toolkit.util.DocFinder.Output;
48
49 public class RestServiceDescription
50 {
51   /**
52    * @param details
53    * @param postUrl
54    * @param urlSuffix
55    * @param inputParams
56    * @param hseparable
57    * @param vseparable
58    * @param gapCharacter
59    */
60   public RestServiceDescription(String action, String description,
61           String name, String postUrl, String urlSuffix,
62           Map<String, InputType> inputParams, boolean hseparable,
63           boolean vseparable, char gapCharacter)
64   {
65     super();
66     this.details = new UIinfo();
67     details.Action = action == null ? "" : action;
68     details.description = description == null ? "" : description;
69     details.Name = name == null ? "" : name;
70     this.postUrl = postUrl == null ? "" : postUrl;
71     this.urlSuffix = urlSuffix == null ? "" : urlSuffix;
72     if (inputParams != null)
73     {
74       this.inputParams = inputParams;
75     }
76     this.hseparable = hseparable;
77     this.vseparable = vseparable;
78     this.gapCharacter = gapCharacter;
79   }
80
81   public boolean equals(Object o)
82   {
83     if (o == null || !(o instanceof RestServiceDescription))
84     {
85       return false;
86     }
87     RestServiceDescription other = (RestServiceDescription) o;
88     boolean diff = (gapCharacter != other.gapCharacter);
89     diff |= vseparable != other.vseparable;
90     diff |= hseparable != other.hseparable;
91     diff |= !(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       return null;
374     java.util.ArrayList<String> jv = new ArrayList<String>();
375     int cp = 0, pos, escape;
376     boolean wasescaped = false,wasquoted=false;
377     String lstitem = null;
378     while ((pos = list.indexOf(separator, cp)) >= cp)
379     {
380       
381       escape = (pos > 0 && list.charAt(pos - 1) == '\\') ? -1 : 0;
382       if (wasescaped || wasquoted)
383       {
384         // append to previous pos
385         jv.set(jv.size() - 1,
386                 lstitem = lstitem + separator
387                         + list.substring(cp, pos + escape));
388
389       }
390       else
391       {
392         jv.add(lstitem = list.substring(cp, pos + escape));
393       }
394       cp = pos + seplen;
395       wasescaped = escape == -1;
396       if (!wasescaped)
397       {
398         // last separator may be in an unmatched quote
399         if (java.util.regex.Pattern.matches("('[^']*')*[^']*'",lstitem))
400         {
401           wasquoted=true;
402         }
403       }
404       
405     }
406     if (cp < list.length())
407     {
408       String c = list.substring(cp);
409       if (wasescaped || wasquoted)
410       {
411         // append final separator
412         jv.set(jv.size() - 1, lstitem + separator + c);
413       }
414       else
415       {
416         if (!c.equals(separator))
417         {
418           jv.add(c);
419         }
420       }
421     }
422     if (jv.size() > 0)
423     {
424       String[] v = jv.toArray(new String[jv.size()]);
425       jv.clear();
426       if (debug)
427       {
428         System.err.println("Array from '" + separator
429                 + "' separated List:\n" + v.length);
430         for (int i = 0; i < v.length; i++)
431         {
432           System.err.println("item " + i + " '" + v[i] + "'");
433         }
434       }
435       return v;
436     }
437     if (debug)
438     {
439       System.err.println("Empty Array from '" + separator
440               + "' separated List");
441     }
442     return null;
443   }
444
445   /**
446    * concatenate the list with separator
447    * 
448    * @param list
449    * @param separator
450    * @return concatenated string
451    */
452   public static String arrayToSeparatorList(String[] list, String separator)
453   {
454     StringBuffer v = new StringBuffer();
455     if (list != null && list.length > 0)
456     {
457       for (int i = 0, iSize = list.length; i < iSize; i++)
458       {
459         if (list[i] != null)
460         {
461           if (v.length() > 0)
462           {
463             v.append(separator);
464           }
465           // TODO - escape any separator values in list[i]
466           v.append(list[i]);
467         }
468       }
469       if (debug)
470       {
471         System.err.println("Returning '" + separator
472                 + "' separated List:\n");
473         System.err.println(v);
474       }
475       return v.toString();
476     }
477     if (debug)
478     {
479       System.err.println("Returning empty '" + separator
480               + "' separated List\n");
481     }
482     return "" + separator;
483   }
484
485   /**
486    * parse a string containing a list of service properties and configure the
487    * service description
488    * 
489    * @param propList
490    *          param warnings a StringBuffer that any warnings about invalid
491    *          content will be appended to.
492    */
493   private boolean configureFromServiceInputProperties(String propList,
494           StringBuffer warnings)
495   {
496     String[] props = separatorListToArray(propList, ",");
497     if (props == null)
498     {
499       return true;
500     }
501     ;
502     boolean valid = true;
503     String val = null;
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         int l = warnings.length();
542         _configureOutputFormatFrom(val, warnings);
543         valid = (l != warnings.length());
544       }
545     }
546     return valid;
547   }
548
549   private String _genOutputFormatString()
550   {
551     String buff = "";
552     if (resultData == null)
553     {
554       return "";
555     }
556     for (JvDataType type : resultData)
557     {
558       if (buff.length() > 0)
559       {
560         buff += ";";
561       }
562       buff += type.toString();
563     }
564     return buff;
565   }
566
567   private void _configureOutputFormatFrom(String outstring,
568           StringBuffer warnings)
569   {
570     if (outstring.indexOf(";") == -1)
571     {
572       // we add a token, for simplicity
573       outstring = outstring + ";";
574     }
575     StringTokenizer st = new StringTokenizer(outstring, ";");
576     String tok = "";
577     resultData = new ArrayList<JvDataType>();
578     while (st.hasMoreTokens())
579     {
580       try
581       {
582         resultData.add(JvDataType.valueOf(tok = st.nextToken()));
583       } catch (NoSuchElementException x)
584       {
585         warnings.append("Invalid result type: '" + tok
586                 + "' (must be one of: ");
587         String sep = "";
588         for (JvDataType vl : JvDataType.values())
589         {
590           warnings.append(sep);
591           warnings.append(vl.toString());
592           sep = " ,";
593         }
594         warnings.append(" separated by semi-colons)\n");
595       }
596     }
597   }
598
599   private String getServiceIOProperties()
600   {
601     String[] vls = new String[]
602     { isHseparable() ? "hseparable" : "",
603         isVseparable() ? "vseparable" : "",
604         (new String("gapCharacter='" + gapCharacter + "'")),
605         (new String("returns='" + _genOutputFormatString() + "'")) };
606
607     return arrayToSeparatorList(vls, ",");
608   }
609
610   public String toString()
611   {
612     StringBuffer result = new StringBuffer();
613     result.append(details.Name);
614     result.append('|');
615     result.append(details.Action);
616     result.append('|');
617     if (details.description != null)
618     {
619       result.append(details.description);
620     }
621     ;
622     // list job input flags
623     result.append('|');
624     result.append(getServiceIOProperties());
625     // list any additional cgi parameters needed for result retrieval
626     if (urlSuffix != null && urlSuffix.length() > 0)
627     {
628       result.append('|');
629       result.append(urlSuffix);
630     }
631     result.append('|');
632     result.append(getInputParamEncodedUrl());
633     return result.toString();
634   }
635
636   public boolean configureFromEncodedString(String encoding,
637           StringBuffer warnings)
638   {
639     boolean invalid = false;
640     String[] list = separatorListToArray(encoding, "|");
641     details.Name = list[0];
642     details.Action = list[1];
643     details.description = list[2];
644     invalid |= !configureFromServiceInputProperties(list[3], warnings);
645     if (list.length > 5)
646     {
647       urlSuffix = list[4];
648       invalid |= !configureFromInputParamEncodedUrl(list[5], warnings);
649     }
650     else
651     {
652       if (list.length > 4)
653       {
654         urlSuffix = null;
655         invalid |= !configureFromInputParamEncodedUrl(list[4], warnings);
656       }
657     }
658     return !invalid;
659   }
660
661   /**
662    * @return string representation of the input parameters, their type and
663    *         constraints, appended to the service's base submission URL
664    */
665   private String getInputParamEncodedUrl()
666   {
667     StringBuffer url = new StringBuffer();
668     if (postUrl == null || postUrl.length() < 5)
669     {
670       return "";
671     }
672
673     url.append(postUrl);
674     char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
675     boolean consts = true;
676     do
677     {
678       for (Map.Entry<String, InputType> param : inputParams.entrySet())
679       {
680         List<String> vals = param.getValue().getURLEncodedParameter();
681         if (param.getValue().isConstant())
682         {
683           if (consts)
684           {
685             url.append(appendChar);
686             appendChar = '&';
687             url.append(param.getValue().token);
688             if (vals.size() == 1)
689             {
690               url.append("=");
691               url.append(vals.get(0));
692             }
693           }
694         }
695         else
696         {
697           if (!consts)
698           {
699             url.append(appendChar);
700             appendChar = '&';
701             url.append(param.getValue().token);
702             url.append("=");
703             // write parameter set as $TOKENPREFIX:csv list of params$ for this
704             // input param
705             url.append("$");
706             url.append(param.getValue().getURLtokenPrefix());
707             url.append(":");
708             url.append(arrayToSeparatorList(vals.toArray(new String[0]),
709                     ","));
710             url.append("$");
711           }
712         }
713
714       }
715       // toggle consts and repeat until !consts is false:
716     } while (!(consts = !consts));
717     return url.toString();
718   }
719
720   /**
721    * parse the service URL and input parameters from the given encoded URL
722    * string and configure the RestServiceDescription from it.
723    * 
724    * @param ipurl
725    * @param warnings
726    *          where any warnings
727    * @return true if URL parsed correctly. false means the configuration failed.
728    */
729   private boolean configureFromInputParamEncodedUrl(String ipurl,
730           StringBuffer warnings)
731   {
732     boolean valid = true;
733     int lastp = 0;
734     String url = new String();
735     Matcher prms = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$")
736             .matcher(ipurl);
737     Map<String, InputType> iparams = new Hashtable<String, InputType>();
738     InputType jinput;
739     while (prms.find())
740     {
741       if (lastp < prms.start(0))
742       {
743         url += ipurl.substring(lastp, prms.start(0));
744         lastp = prms.end(0) + 1;
745       }
746       String sep = prms.group(1);
747       String tok = prms.group(2);
748       String iprm = prms.group(3);
749       int colon = iprm.indexOf(":");
750       String iprmparams = "";
751       if (colon > -1)
752       {
753         iprmparams = iprm.substring(colon + 1);
754         iprm = iprm.substring(0, colon);
755       }
756       valid = parseTypeString(prms.group(0), tok, iprm, iprmparams,
757               iparams, warnings);
758     }
759     if (valid)
760     {
761       try
762       {
763         URL u = new URL(url);
764         postUrl = url;
765         inputParams = iparams;
766       } catch (Exception e)
767       {
768         warnings.append("Failed to parse '" + url + "' as a URL.\n");
769         valid = false;
770       }
771     }
772     return valid;
773   }
774
775   public static Class[] getInputTypes()
776   {
777     // TODO - find a better way of maintaining this classlist
778     return new Class[]
779     { jalview.ws.rest.params.Alignment.class,
780         jalview.ws.rest.params.AnnotationFile.class,
781         SeqGroupIndexVector.class,
782         jalview.ws.rest.params.SeqIdVector.class,
783         jalview.ws.rest.params.SeqVector.class,
784         jalview.ws.rest.params.Tree.class };
785   }
786
787   public static boolean parseTypeString(String fullstring, String tok,
788           String iprm, String iprmparams, Map<String, InputType> iparams,
789           StringBuffer warnings)
790   {
791     boolean valid = true;
792     InputType jinput;
793     for (Class type : getInputTypes())
794     {
795       try
796       {
797         jinput = (InputType) (type.getConstructor().newInstance(null));
798         if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
799         {
800           ArrayList<String> al = new ArrayList<String>();
801           for (String prprm : separatorListToArray(iprmparams, ","))
802           {
803             // hack to ensure that strings like "sep=','" containing unescaped commas as values are concatenated
804             al.add(prprm.trim());
805           }
806           if (!jinput.configureFromURLtokenString(al, warnings))
807           {
808             valid = false;
809             warnings.append("Failed to parse '" + fullstring + "' as a "
810                     + jinput.getURLtokenPrefix() + " input tag.\n");
811           }
812           else
813           {
814             jinput.token = tok;
815             iparams.put(tok, jinput);
816             valid = true;
817           }
818           break;
819         }
820
821       } catch (Throwable thr)
822       {
823       }
824       ;
825     }
826     return valid;
827   }
828
829   public static void main(String argv[])
830   {
831     // test separator list
832     try {
833       assert(separatorListToArray("foo=',',min='foo',max='1,2,3',fa=','", ",").length==4);
834       if (separatorListToArray("minsize='2', sep=','", ",").length==2)
835       {
836         assert(false);
837       }
838       
839     } catch (AssertionError x)
840     {
841       System.err.println("separatorListToArray is faulty.");
842     }
843     if (argv.length == 0)
844     {
845       if (!testRsdExchange("Test using default Shmmr service",
846               RestClient.makeShmmrRestClient().service))
847       {
848         System.err.println("default test failed.");
849       }
850       else
851       {
852         System.err.println("default test passed.");
853       }
854     }
855     else
856     {
857       int i = 0, p = 0;
858       for (String svc : argv)
859       {
860         p += testRsdExchange("Test " + (++i), svc) ? 1 : 0;
861       }
862       System.err.println("" + p + " out of " + i + " tests passed.");
863
864     }
865   }
866
867   private static boolean testRsdExchange(String desc, String servicestring)
868   {
869     try
870     {
871       RestServiceDescription newService = new RestServiceDescription(
872               servicestring);
873       if (!newService.isValid())
874       {
875         throw new Error("Failed to create service from '" + servicestring
876                 + "'.\n" + newService.getInvalidMessage());
877       }
878       return testRsdExchange(desc, newService);
879     } catch (Throwable x)
880     {
881       System.err.println("Failed for service (" + desc + "): "
882               + servicestring);
883       x.printStackTrace();
884       return false;
885     }
886   }
887
888   private static boolean testRsdExchange(String desc,
889           RestServiceDescription service)
890   {
891     try
892     {
893       String fromservicetostring = service.toString();
894       RestServiceDescription newService = new RestServiceDescription(
895               fromservicetostring);
896       if (!newService.isValid())
897       {
898         throw new Error("Failed to create service from '"
899                 + fromservicetostring + "'.\n"
900                 + newService.getInvalidMessage());
901       }
902
903       if (!service.equals(newService))
904       {
905         System.err.println("Failed for service (" + desc + ").");
906         System.err.println("Original service and parsed service differ.");
907         System.err.println("Original: " + fromservicetostring);
908         System.err.println("Parsed  : " + newService.toString());
909         return false;
910       }
911     } catch (Throwable x)
912     {
913       System.err.println("Failed for service (" + desc + "): "
914               + service.toString());
915       x.printStackTrace();
916       return false;
917     }
918     return true;
919   }
920
921   /**
922    * covenience method to generate the id and sequence string vector from a set
923    * of seuqences using each sequence's getName() and getSequenceAsString()
924    * method
925    * 
926    * @param seqs
927    * @return String[][] {{sequence ids},{sequence strings}}
928    */
929   public static String[][] formStrings(SequenceI[] seqs)
930   {
931     String[][] idset = new String[2][seqs.length];
932     for (int i = 0; i < seqs.length; i++)
933     {
934       idset[0][i] = seqs[i].getName();
935       idset[1][i] = seqs[i].getSequenceAsString();
936     }
937     return idset;
938   }
939
940   /**
941    * can this service be run on the visible portion of an alignment regardless
942    * of hidden boundaries ?
943    */
944   boolean hseparable = false;
945
946   boolean vseparable = false;
947
948   public boolean isHseparable()
949   {
950     return hseparable;
951   }
952
953   /**
954    * 
955    * @return
956    */
957   public boolean isVseparable()
958   {
959     return vseparable;
960   }
961
962   /**
963    * search the input types for an instance of the given class
964    * 
965    * @param <validInput.inputType> class1
966    * @return
967    */
968   public boolean inputInvolves(Class<?> class1)
969   {
970     assert (InputType.class.isAssignableFrom(class1));
971     for (InputType val : inputParams.values())
972     {
973       if (class1.isAssignableFrom(val.getClass()))
974       {
975         return true;
976       }
977     }
978     return false;
979   }
980
981   char gapCharacter = '-';
982
983   /**
984    * 
985    * @return the preferred gap character for alignments input/output by this
986    *         service
987    */
988   public char getGapCharacter()
989   {
990     return gapCharacter;
991   }
992
993   public String getDecoratedResultUrl(String jobId)
994   {
995     // TODO: correctly write ?/& appropriate to result URL format.
996     return jobId + urlSuffix;
997   }
998
999   private List<JvDataType> resultData = new ArrayList<JvDataType>();
1000
1001   /**
1002    * 
1003    * 
1004    * TODO: Extend to optionally specify relative/absolute url where data of this
1005    * type can be retrieved from
1006    * 
1007    * @param dt
1008    */
1009   public void addResultDatatype(JvDataType dt)
1010   {
1011     if (resultData == null)
1012     {
1013       resultData = new ArrayList<JvDataType>();
1014     }
1015     resultData.add(dt);
1016   }
1017
1018   public boolean removeRsultDatatype(JvDataType dt)
1019   {
1020     if (resultData != null)
1021     {
1022       return resultData.remove(dt);
1023     }
1024     return false;
1025   }
1026
1027   public List<JvDataType> getResultDataTypes()
1028   {
1029     return resultData;
1030   }
1031
1032 }