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