dea7fc9347c2d83f8d56f28da0486d26950d3e94
[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;
68     details.description = description;
69     details.Name = name;
70     this.postUrl = postUrl;
71     this.urlSuffix = urlSuffix;
72     this.inputParams = inputParams;
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     String Action;
105
106     String Name;
107
108     String description;
109   }
110
111   public UIinfo details = new UIinfo();
112
113   /**
114    * Service base URL
115    */
116   String postUrl;
117
118   /**
119    * suffix that should be added to any url used if it does not already end in
120    * the suffix.
121    */
122   String urlSuffix;
123
124   /**
125    * input info given as key/value pairs - mapped to post arguments
126    */
127   Map<String, InputType> inputParams = new HashMap();
128
129   /**
130    * assigns the given inputType it to its corresponding input parameter token
131    * it.token
132    * 
133    * @param it
134    */
135   public void setInputParam(InputType it)
136   {
137     inputParams.put(it.token, it);
138   }
139
140   /**
141    * remove the given input type it from the set of service input parameters.
142    * 
143    * @param it
144    */
145   public void removeInputParam(InputType it)
146   {
147     inputParams.remove(it.token);
148   }
149
150   /**
151    * service requests alignment data
152    */
153   boolean aligndata;
154
155   /**
156    * service requests alignment and/or seuqence annotationo data
157    */
158   boolean annotdata;
159
160   /**
161    * service requests partitions defined over input (alignment) data
162    */
163   boolean partitiondata;
164
165   /**
166    * process ths input data and set the appropriate shorthand flags describing
167    * the input the service wants
168    */
169   public void setInvolvesFlags()
170   {
171     aligndata = inputInvolves(Alignment.class);
172     annotdata = inputInvolves(AnnotationFile.class);
173     partitiondata = inputInvolves(SeqGroupIndexVector.class);
174   }
175
176   /**
177    * Service return info { alignment, annotation file (loaded back on to
178    * alignment), tree (loaded back on to alignment), sequence annotation -
179    * loaded back on to alignment), text report, pdb structures with sequence
180    * mapping )
181    * 
182    */
183
184   /**
185    * Start with bare minimum: input is alignment + groups on alignment
186    * 
187    * @author JimP
188    * 
189    */
190
191   private String invalidMessage = null;
192
193   /**
194    * parse the given linkString of the form '<label>|<url>|separator
195    * char[|optional sequence separator char]' into parts. url may contain a
196    * string $SEQUENCEIDS<=optional regex=>$ where <=optional regex=> must be of
197    * the form =/<perl style regex>/=$ or $SEQUENCES<=optional regex=>$ or
198    * $SEQUENCES<=optional regex=>$.
199    * 
200    * @param link
201    */
202   public RestServiceDescription(String link)
203   {
204     StringBuffer warnings = new StringBuffer();
205     if (!configureFromEncodedString(link, warnings))
206     {
207       if (warnings.length() > 0)
208       {
209         invalidMessage = warnings.toString();
210       }
211     }
212   }
213
214   public RestServiceDescription(RestServiceDescription toedit)
215   {
216     if (toedit == null)
217     {
218       return;
219     }
220     // TODO Implement copy constructor NOW
221   }
222
223   /**
224    * @return the invalidMessage
225    */
226   public String getInvalidMessage()
227   {
228     return invalidMessage;
229   }
230
231   /**
232    * Check if URL string was parsed properly.
233    * 
234    * @return boolean - if false then <code>getInvalidMessage</code> returns an
235    *         error message
236    */
237   public boolean isValid()
238   {
239     return invalidMessage == null;
240   }
241
242   private static boolean debug = false;
243
244   /**
245    * parse the string into a list
246    * 
247    * @param list
248    * @param separator
249    * @return elements separated by separator
250    */
251   public static String[] separatorListToArray(String list, String separator)
252   {
253     int seplen = separator.length();
254     if (list == null || list.equals("") || list.equals(separator))
255       return null;
256     java.util.ArrayList<String> jv = new ArrayList<String>();
257     int cp = 0, pos, escape;
258     boolean wasescaped = false;
259     String lstitem = null;
260     while ((pos = list.indexOf(separator, cp)) > cp)
261     {
262       escape = (list.charAt(pos - 1) == '\\') ? -1 : 0;
263       if (wasescaped)
264       {
265         // append to previous pos
266         jv.set(jv.size() - 1,
267                 lstitem = lstitem + separator
268                         + list.substring(cp, pos + escape));
269
270       }
271       else
272       {
273         jv.add(lstitem = list.substring(cp, pos + escape));
274       }
275       cp = pos + seplen;
276       wasescaped = escape == -1;
277     }
278     if (cp < list.length())
279     {
280       String c = list.substring(cp);
281       if (wasescaped)
282       {
283         // append final separator
284         jv.set(jv.size() - 1, lstitem + separator + c);
285       }
286       else
287       {
288         if (!c.equals(separator))
289         {
290           jv.add(c);
291         }
292       }
293     }
294     if (jv.size() > 0)
295     {
296       String[] v = jv.toArray(new String[jv.size()]);
297       jv.clear();
298       if (debug)
299       {
300         System.err.println("Array from '" + separator
301                 + "' separated List:\n" + v.length);
302         for (int i = 0; i < v.length; i++)
303         {
304           System.err.println("item " + i + " '" + v[i] + "'");
305         }
306       }
307       return v;
308     }
309     if (debug)
310     {
311       System.err.println("Empty Array from '" + separator
312               + "' separated List");
313     }
314     return null;
315   }
316
317   /**
318    * concatenate the list with separator
319    * 
320    * @param list
321    * @param separator
322    * @return concatenated string
323    */
324   public static String arrayToSeparatorList(String[] list, String separator)
325   {
326     StringBuffer v = new StringBuffer();
327     if (list != null && list.length > 0)
328     {
329       for (int i = 0, iSize = list.length; i < iSize; i++)
330       {
331         if (list[i] != null)
332         {
333           if (i > 0)
334           {
335             v.append(separator);
336           }
337           // TODO - escape any separator values in list[i]
338           v.append(list[i]);
339         }
340       }
341       if (debug)
342       {
343         System.err.println("Returning '" + separator
344                 + "' separated List:\n");
345         System.err.println(v);
346       }
347       return v.toString();
348     }
349     if (debug)
350     {
351       System.err.println("Returning empty '" + separator
352               + "' separated List\n");
353     }
354     return "" + separator;
355   }
356
357   /**
358    * parse a string containing a list of service properties and configure the
359    * service description
360    * 
361    * @param propList
362    *          param warnings a StringBuffer that any warnings about invalid
363    *          content will be appended to.
364    */
365   private void configureFromServiceInputProperties(String propList,
366           StringBuffer warnings)
367   {
368     String[] props = separatorListToArray(propList, ",");
369     if (props == null)
370     {
371       return;
372     }
373     ;
374     String val = null;
375     int i;
376     for (String prop : props)
377     {
378       if ((i = prop.indexOf("=")) > -1)
379       {
380         val = prop.substring(i + 1);
381         if (val.startsWith("\'") && val.endsWith("\'"))
382         {
383           val = val.substring(1, val.length() - 1);
384         }
385         prop = prop.substring(0, i);
386       }
387
388       if (prop.equals("hseparable"))
389       {
390         hseparable = true;
391       }
392       if (prop.equals("vseparable"))
393       {
394         vseparable = true;
395       }
396       if (prop.equals("gapCharacter"))
397       {
398         if (val == null || val.length() > 1)
399         {
400           warnings.append((warnings.length() > 0 ? "\n" : "")
401                   + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
402                           + val + "'"));
403         }
404         else
405         {
406           gapCharacter = val.charAt(0);
407         }
408       }
409       if (prop.equals("returns"))
410       {
411         _configureOurputFormatFrom(val, warnings);
412       }
413     }
414   }
415
416   private String _genOutputFormatString()
417   {
418     String buff = "";
419     if (resultData==null)
420     {
421       return "";
422     }
423     for (JvDataType type : resultData)
424     {
425       if (buff.length() > 0)
426       {
427         buff += ";";
428       }
429       buff += type.toString();
430     }
431     return buff;
432   }
433
434   private void _configureOurputFormatFrom(String outstring,
435           StringBuffer warnings)
436   {
437     StringTokenizer st = new StringTokenizer(outstring, ";");
438     String tok = "";
439     resultData = new ArrayList<JvDataType>();
440     while (st.hasMoreTokens())
441     {
442       try
443       {
444         resultData.add(JvDataType.valueOf(tok = st.nextToken()));
445       } catch (NoSuchElementException x)
446       {
447         warnings.append("Invalid result type: '" + tok
448                 + "' (must be one of: ");
449         String sep = "";
450         for (JvDataType vl : JvDataType.values())
451         {
452           warnings.append(sep);
453           warnings.append(vl.toString());
454           sep = " ,";
455         }
456         warnings.append(" separated by semi-colons)\n");
457       }
458     }
459   }
460
461   private String getServiceIOProperties()
462   {
463     String[] vls = new String[]
464     { isHseparable() ? "hseparable" : "",
465         isVseparable() ? "vseparable" : "",
466         (new String("gapCharacter='" + gapCharacter + "'")),
467         (new String("returns='" + _genOutputFormatString() + "'")) };
468
469     return arrayToSeparatorList(vls, ",");
470   }
471
472   public String toString()
473   {
474     StringBuffer result = new StringBuffer();
475     result.append(details.Name);
476     result.append('|');
477     result.append(details.Action);
478     result.append('|');
479     if (details.description != null)
480     {
481       result.append(details.description);
482     }
483     ;
484     // list job input flags
485     result.append('|');
486     result.append(getServiceIOProperties());
487     // list any additional cgi parameters needed for result retrieval
488     if (urlSuffix != null && urlSuffix.length() > 0)
489     {
490       result.append('|');
491       result.append(urlSuffix);
492     }
493     result.append('|');
494     result.append(getInputParamEncodedUrl());
495     return result.toString();
496   }
497
498   public boolean configureFromEncodedString(String encoding,
499           StringBuffer warnings)
500   {
501     boolean valid = false;
502     String[] list = separatorListToArray(encoding, "|");
503     details.Name = list[0];
504     details.Action = list[1];
505     details.description = list[2];
506     configureFromServiceInputProperties(list[3], warnings);
507     if (list.length > 5)
508     {
509       urlSuffix = list[4];
510       valid |= configureFromInputParamEncodedUrl(list[5], warnings);
511     }
512     else
513     {
514       urlSuffix = null;
515       valid |= configureFromInputParamEncodedUrl(list[4], warnings);
516     }
517     return valid;
518   }
519
520   /**
521    * @return string representation of the input parameters, their type and
522    *         constraints, appended to the service's base submission URL
523    */
524   private String getInputParamEncodedUrl()
525   {
526     StringBuffer url = new StringBuffer();
527     if (postUrl == null || postUrl.length() < 5)
528     {
529       return "";
530     }
531
532     url.append(postUrl);
533     char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
534     boolean consts = true;
535     do
536     {
537       for (Map.Entry<String, InputType> param : inputParams.entrySet())
538       {
539         List<String> vals = param.getValue().getURLEncodedParameter();
540         if (param.getValue().isConstant())
541         {
542           if (consts)
543           {
544             url.append(appendChar);
545             appendChar = '&';
546             url.append(param.getValue().token);
547             if (vals.size() == 1)
548             {
549               url.append("=");
550               url.append(vals.get(0));
551             }
552           }
553         }
554         else
555         {
556           if (!consts)
557           {
558             url.append(appendChar);
559             appendChar = '&';
560             url.append(param.getValue().token);
561             url.append("=");
562             // write parameter set as $TOKENPREFIX:csv list of params$ for this
563             // input param
564             url.append("$");
565             url.append(param.getValue().getURLtokenPrefix());
566             url.append(":");
567             url.append(arrayToSeparatorList(vals.toArray(new String[0]),
568                     ","));
569             url.append("$");
570           }
571         }
572
573       }
574       // toggle consts and repeat until !consts is false:
575     } while (!(consts = !consts));
576     return url.toString();
577   }
578
579   /**
580    * parse the service URL and input parameters from the given encoded URL
581    * string and configure the RestServiceDescription from it.
582    * 
583    * @param ipurl
584    * @param warnings
585    *          where any warnings
586    * @return true if URL parsed correctly. false means the configuration failed.
587    */
588   private boolean configureFromInputParamEncodedUrl(String ipurl,
589           StringBuffer warnings)
590   {
591     boolean valid = true;
592     int lastp = 0;
593     String url = new String();
594     Matcher prms = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$")
595             .matcher(ipurl);
596     Map<String, InputType> iparams = new Hashtable<String, InputType>();
597     InputType jinput;
598     while (prms.find())
599     {
600       if (lastp < prms.start(0))
601       {
602         url += ipurl.substring(lastp, prms.start(0));
603         lastp = prms.end(0) + 1;
604       }
605       String sep = prms.group(1);
606       String tok = prms.group(2);
607       String iprm = prms.group(3);
608       int colon = iprm.indexOf(":");
609       String iprmparams = "";
610       if (colon > -1)
611       {
612         iprmparams = iprm.substring(colon + 1);
613         iprm = iprm.substring(0, colon);
614       }
615       // TODO - find a better way of maintaining this classlist
616       for (Class type : new Class[]
617       { jalview.ws.rest.params.Alignment.class,
618           jalview.ws.rest.params.AnnotationFile.class,
619           SeqGroupIndexVector.class,
620           jalview.ws.rest.params.SeqIdVector.class,
621           jalview.ws.rest.params.SeqVector.class,
622           jalview.ws.rest.params.Tree.class })
623       {
624         try
625         {
626           jinput = (InputType) (type.getConstructor().newInstance(null));
627           if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
628           {
629             ArrayList<String> al = new ArrayList<String>();
630             for (String prprm : separatorListToArray(iprmparams, ","))
631             {
632               al.add(prprm);
633             }
634             if (!jinput.configureFromURLtokenString(al, warnings))
635             {
636               valid = false;
637               warnings.append("Failed to parse '" + prms.group(0)
638                       + "' as a " + jinput.getURLtokenPrefix()
639                       + " input tag.\n");
640             }
641             else
642             {
643               jinput.token = tok;
644               iparams.put(tok, jinput);
645             }
646             break;
647           }
648
649         } catch (Throwable thr)
650         {
651         }
652         ;
653       }
654     }
655     if (valid)
656     {
657       try
658       {
659         URL u = new URL(url);
660         postUrl = url;
661         inputParams = iparams;
662       } catch (Exception e)
663       {
664         warnings.append("Failed to parse '" + url + "' as a URL.\n");
665         valid = false;
666       }
667     }
668     return valid;
669   }
670
671   public static void main(String argv[])
672   {
673     if (argv.length == 0)
674     {
675       if (!testRsdExchange("Test using default Shmmr service",
676               RestClient.makeShmmrRestClient().service))
677       {
678         System.err.println("default test failed.");
679       }
680       else
681       {
682         System.err.println("default test passed.");
683       }
684     }
685     else
686     {
687       int i = 0, p = 0;
688       for (String svc : argv)
689       {
690         p += testRsdExchange("Test " + (++i), svc) ? 1 : 0;
691       }
692       System.err.println("" + p + " out of " + i + " tests passed.");
693
694     }
695   }
696
697   private static boolean testRsdExchange(String desc, String servicestring)
698   {
699     try
700     {
701       RestServiceDescription newService = new RestServiceDescription(
702               servicestring);
703       if (!newService.isValid())
704       {
705         throw new Error("Failed to create service from '" + servicestring
706                 + "'.\n" + newService.getInvalidMessage());
707       }
708       return testRsdExchange(desc, newService);
709     } catch (Throwable x)
710     {
711       System.err.println("Failed for service (" + desc + "): "
712               + servicestring);
713       x.printStackTrace();
714       return false;
715     }
716   }
717
718   private static boolean testRsdExchange(String desc,
719           RestServiceDescription service)
720   {
721     try
722     {
723       String fromservicetostring = service.toString();
724       RestServiceDescription newService = new RestServiceDescription(
725               fromservicetostring);
726       if (!newService.isValid())
727       {
728         throw new Error("Failed to create service from '"
729                 + fromservicetostring + "'.\n"
730                 + newService.getInvalidMessage());
731       }
732
733       if (!service.equals(newService))
734       {
735         System.err.println("Failed for service (" + desc + ").");
736         System.err.println("Original service and parsed service differ.");
737         System.err.println("Original: " + fromservicetostring);
738         System.err.println("Parsed  : " + newService.toString());
739         return false;
740       }
741     } catch (Throwable x)
742     {
743       System.err.println("Failed for service (" + desc + "): "
744               + service.toString());
745       x.printStackTrace();
746       return false;
747     }
748     return true;
749   }
750
751   /**
752    * covenience method to generate the id and sequence string vector from a set
753    * of seuqences using each sequence's getName() and getSequenceAsString()
754    * method
755    * 
756    * @param seqs
757    * @return String[][] {{sequence ids},{sequence strings}}
758    */
759   public static String[][] formStrings(SequenceI[] seqs)
760   {
761     String[][] idset = new String[2][seqs.length];
762     for (int i = 0; i < seqs.length; i++)
763     {
764       idset[0][i] = seqs[i].getName();
765       idset[1][i] = seqs[i].getSequenceAsString();
766     }
767     return idset;
768   }
769
770   /**
771    * can this service be run on the visible portion of an alignment regardless
772    * of hidden boundaries ?
773    */
774   boolean hseparable = false;
775
776   boolean vseparable = false;
777
778   public boolean isHseparable()
779   {
780     return hseparable;
781   }
782
783   /**
784    * 
785    * @return
786    */
787   public boolean isVseparable()
788   {
789     return vseparable;
790   }
791
792   /**
793    * search the input types for an instance of the given class
794    * 
795    * @param <validInput.inputType> class1
796    * @return
797    */
798   public boolean inputInvolves(Class<?> class1)
799   {
800     assert (InputType.class.isAssignableFrom(class1));
801     for (InputType val : inputParams.values())
802     {
803       if (class1.isAssignableFrom(val.getClass()))
804       {
805         return true;
806       }
807     }
808     return false;
809   }
810
811   char gapCharacter = '-';
812
813   /**
814    * 
815    * @return the preferred gap character for alignments input/output by this
816    *         service
817    */
818   public char getGapCharacter()
819   {
820     return gapCharacter;
821   }
822
823   public String getDecoratedResultUrl(String jobId)
824   {
825     // TODO: correctly write ?/& appropriate to result URL format.
826     return jobId + urlSuffix;
827   }
828
829   private List<JvDataType> resultData;
830
831   /**
832    * 
833    * 
834    * TODO: Extend to optionally specify relative/absolute url where data of this
835    * type can be retrieved from
836    * 
837    * @param dt
838    */
839   public void addResultDatatype(JvDataType dt)
840   {
841     if (resultData == null)
842     {
843       resultData = new ArrayList<JvDataType>();
844     }
845     resultData.add(dt);
846   }
847
848   public boolean removeRsultDatatype(JvDataType dt)
849   {
850     if (resultData != null)
851     {
852       return resultData.remove(dt);
853     }
854     return false;
855   }
856
857   public List<JvDataType> getResultDataTypes()
858   {
859     return resultData;
860   }
861 }