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