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