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