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