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