926897564822b642c184868aa62b775ed5ffec1f
[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     details = new UIinfo(list[p + 1], list[p + 1], list[p], list[p + 2],
525             postUrl);
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     return invalid ? -1 : p;
546   }
547
548   /**
549    * @return string representation of the input parameters, their type and
550    *         constraints, appended to the service's base submission URL
551    */
552   private String getInputParamEncodedUrl()
553   {
554     StringBuffer url = new StringBuffer();
555     if (postUrl == null || postUrl.length() < 5)
556     {
557       return "";
558     }
559
560     url.append(postUrl);
561     char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
562     boolean consts = true;
563     do
564     {
565       for (Map.Entry<String, InputType> param : inputParams.entrySet())
566       {
567         List<String> vals = param.getValue().getURLEncodedParameter();
568         if (param.getValue().isConstant())
569         {
570           if (consts)
571           {
572             url.append(appendChar);
573             appendChar = '&';
574             url.append(param.getValue().token);
575             if (vals.size() == 1)
576             {
577               url.append("=");
578               url.append(vals.get(0));
579             }
580           }
581         }
582         else
583         {
584           if (!consts)
585           {
586             url.append(appendChar);
587             appendChar = '&';
588             url.append(param.getValue().token);
589             url.append("=");
590             // write parameter set as $TOKENPREFIX:csv list of params$ for this
591             // input param
592             url.append("$");
593             url.append(param.getValue().getURLtokenPrefix());
594             url.append(":");
595             url.append(StringUtils.arrayToSeparatorList(
596                     vals.toArray(new String[0]), ","));
597             url.append("$");
598           }
599         }
600
601       }
602       // toggle consts and repeat until !consts is false:
603     } while (!(consts = !consts));
604     return url.toString();
605   }
606
607   /**
608    * parse the service URL and input parameters from the given encoded URL
609    * string and configure the RestServiceDescription from it.
610    * 
611    * @param ipurl
612    * @param warnings
613    *          where any warnings
614    * @return true if URL parsed correctly. false means the configuration failed.
615    */
616   private boolean configureFromInputParamEncodedUrl(String ipurl,
617           StringBuffer warnings)
618   {
619     boolean valid = true;
620     int lastp = 0;
621     String url = new String();
622     Matcher prms = PARAM_ENCODED_URL_PATTERN.matcher(ipurl);
623     Map<String, InputType> iparams = new Hashtable<>();
624     InputType jinput;
625     while (prms.find())
626     {
627       if (lastp < prms.start(0))
628       {
629         url += ipurl.substring(lastp, prms.start(0));
630         lastp = prms.end(0) + 1;
631       }
632       String sep = prms.group(1);
633       String tok = prms.group(2);
634       String iprm = prms.group(3);
635       int colon = iprm.indexOf(":");
636       String iprmparams = "";
637       if (colon > -1)
638       {
639         iprmparams = iprm.substring(colon + 1);
640         iprm = iprm.substring(0, colon);
641       }
642       valid = parseTypeString(prms.group(0), tok, iprm, iprmparams, iparams,
643               warnings);
644     }
645     if (valid)
646     {
647       try
648       {
649         URL u = new URL(url);
650         postUrl = url;
651         inputParams = iparams;
652       } catch (Exception e)
653       {
654         warnings.append("Failed to parse '" + url + "' as a URL.\n");
655         valid = false;
656       }
657     }
658     return valid;
659   }
660
661   public static Class[] getInputTypes()
662   {
663     // TODO - find a better way of maintaining this classlist
664     return new Class[] { jalview.ws.rest.params.Alignment.class,
665         jalview.ws.rest.params.AnnotationFile.class,
666         SeqGroupIndexVector.class, jalview.ws.rest.params.SeqIdVector.class,
667         jalview.ws.rest.params.SeqVector.class,
668         jalview.ws.rest.params.Tree.class };
669   }
670
671   public static boolean parseTypeString(String fullstring, String tok,
672           String iprm, String iprmparams, Map<String, InputType> iparams,
673           StringBuffer warnings)
674   {
675     boolean valid = true;
676     InputType jinput;
677     for (Class type : getInputTypes())
678     {
679       try
680       {
681         jinput = (InputType) (type.getConstructor().newInstance());
682         if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
683         {
684           ArrayList<String> al = new ArrayList<>();
685           for (String prprm : StringUtils.separatorListToArray(iprmparams,
686                   ","))
687           {
688             // hack to ensure that strings like "sep=','" containing unescaped
689             // commas as values are concatenated
690             al.add(prprm.trim());
691           }
692           if (!jinput.configureFromURLtokenString(al, warnings))
693           {
694             valid = false;
695             warnings.append("Failed to parse '" + fullstring + "' as a "
696                     + jinput.getURLtokenPrefix() + " input tag.\n");
697           }
698           else
699           {
700             jinput.token = tok;
701             iparams.put(tok, jinput);
702             valid = true;
703           }
704           break;
705         }
706
707       } catch (Throwable thr)
708       {
709       }
710       ;
711     }
712     return valid;
713   }
714
715   /**
716    * covenience method to generate the id and sequence string vector from a set
717    * of seuqences using each sequence's getName() and getSequenceAsString()
718    * method
719    * 
720    * @param seqs
721    * @return String[][] {{sequence ids},{sequence strings}}
722    */
723   public static String[][] formStrings(SequenceI[] seqs)
724   {
725     String[][] idset = new String[2][seqs.length];
726     for (int i = 0; i < seqs.length; i++)
727     {
728       idset[0][i] = seqs[i].getName();
729       idset[1][i] = seqs[i].getSequenceAsString();
730     }
731     return idset;
732   }
733
734   /**
735    * can this service be run on the visible portion of an alignment regardless
736    * of hidden boundaries ?
737    */
738   boolean hseparable = false;
739
740   boolean vseparable = false;
741
742   public boolean isHseparable()
743   {
744     return hseparable;
745   }
746
747   /**
748    * 
749    * @return
750    */
751   public boolean isVseparable()
752   {
753     return vseparable;
754   }
755
756   /**
757    * search the input types for an instance of the given class
758    * 
759    * @param <validInput.inputType>
760    *          class1
761    * @return
762    */
763   public boolean inputInvolves(Class<?> class1)
764   {
765     assert (InputType.class.isAssignableFrom(class1));
766     for (InputType val : inputParams.values())
767     {
768       if (class1.isAssignableFrom(val.getClass()))
769       {
770         return true;
771       }
772     }
773     return false;
774   }
775
776   char gapCharacter = '-';
777
778   /**
779    * 
780    * @return the preferred gap character for alignments input/output by this
781    *         service
782    */
783   public char getGapCharacter()
784   {
785     return gapCharacter;
786   }
787
788   public String getDecoratedResultUrl(String jobId)
789   {
790     // TODO: correctly write ?/& appropriate to result URL format.
791     return jobId + urlSuffix;
792   }
793
794   private List<JvDataType> resultData = new ArrayList<>();
795
796   /**
797    * 
798    * 
799    * TODO: Extend to optionally specify relative/absolute url where data of this
800    * type can be retrieved from
801    * 
802    * @param dt
803    */
804   public void addResultDatatype(JvDataType dt)
805   {
806     if (resultData == null)
807     {
808       resultData = new ArrayList<>();
809     }
810     resultData.add(dt);
811   }
812
813   public boolean removeRsultDatatype(JvDataType dt)
814   {
815     if (resultData != null)
816     {
817       return resultData.remove(dt);
818     }
819     return false;
820   }
821
822   public List<JvDataType> getResultDataTypes()
823   {
824     return resultData;
825   }
826
827   /**
828    * parse a concatenated list of rest service descriptions into an array
829    * 
830    * @param services
831    * @return zero or more services.
832    * @throws exceptions
833    *           if the services are improperly encoded.
834    */
835   public static List<RestServiceDescription> parseDescriptions(
836           String services) throws Exception
837   {
838     String[] list = StringUtils.separatorListToArray(services, "|");
839     List<RestServiceDescription> svcparsed = new ArrayList<>();
840     int p = 0, lastp = 0;
841     StringBuffer warnings = new StringBuffer();
842     do
843     {
844       RestServiceDescription rsd = new RestServiceDescription();
845       p = rsd.parseServiceList(list, warnings, lastp = p);
846       if (p > lastp && rsd.isValid())
847       {
848         svcparsed.add(rsd);
849       }
850       else
851       {
852         throw new Exception(
853                 "Failed to parse user defined RSBS services from :"
854                         + services
855                         + "\nFirst error was encountered at token " + lastp
856                         + " starting " + list[lastp] + ":\n"
857                         + rsd.getInvalidMessage());
858       }
859     } while (p < lastp && p < list.length - 1);
860     return svcparsed;
861   }
862
863 }