2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 package jalview.ws.rest;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.Hashtable;
27 import java.util.List;
29 import java.util.NoSuchElementException;
30 import java.util.StringTokenizer;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
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;
41 public class RestServiceDescription
43 private static final Pattern PARAM_ENCODED_URL_PATTERN = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$");
46 * create a new rest service description ready to be configured
48 public RestServiceDescription()
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)
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)
76 this.inputParams = inputParams;
78 this.hseparable = hseparable;
79 this.vseparable = vseparable;
80 this.gapCharacter = gapCharacter;
84 public boolean equals(Object o)
86 if (o == null || !(o instanceof RestServiceDescription))
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);
106 * Service UI Info { Action, Specific Name of Service, Brief Description }
111 public String getAction()
116 public void setAction(String action)
121 public String getName()
126 public void setName(String name)
131 public String getDescription()
136 public void setDescription(String description)
138 this.description = description;
148 public UIinfo details = new UIinfo();
150 public String getAction()
152 return details.getAction();
155 public void setAction(String action)
157 details.setAction(action);
160 public String getName()
162 return details.getName();
165 public void setName(String name)
167 details.setName(name);
170 public String getDescription()
172 return details.getDescription();
175 public void setDescription(String description)
177 details.setDescription(description);
185 public String getPostUrl()
190 public void setPostUrl(String postUrl)
192 this.postUrl = postUrl;
195 public String getUrlSuffix()
200 public void setUrlSuffix(String urlSuffix)
202 this.urlSuffix = urlSuffix;
205 public Map<String, InputType> getInputParams()
210 public void setInputParams(Map<String, InputType> inputParams)
212 this.inputParams = inputParams;
215 public void setHseparable(boolean hseparable)
217 this.hseparable = hseparable;
220 public void setVseparable(boolean vseparable)
222 this.vseparable = vseparable;
225 public void setGapCharacter(char gapCharacter)
227 this.gapCharacter = gapCharacter;
231 * suffix that should be added to any url used if it does not already end in
237 * input info given as key/value pairs - mapped to post arguments
239 Map<String, InputType> inputParams = new HashMap<String, InputType>();
242 * assigns the given inputType it to its corresponding input parameter token
247 public void setInputParam(InputType it)
249 inputParams.put(it.token, it);
253 * remove the given input type it from the set of service input parameters.
257 public void removeInputParam(InputType it)
259 inputParams.remove(it.token);
263 * service requests alignment data
268 * service requests alignment and/or seuqence annotationo data
273 * service requests partitions defined over input (alignment) data
275 boolean partitiondata;
278 * process ths input data and set the appropriate shorthand flags describing
279 * the input the service wants
281 public void setInvolvesFlags()
283 aligndata = inputInvolves(Alignment.class);
284 annotdata = inputInvolves(AnnotationFile.class);
285 partitiondata = inputInvolves(SeqGroupIndexVector.class);
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
297 * Start with bare minimum: input is alignment + groups on alignment
303 private String invalidMessage = null;
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=>$.
314 public RestServiceDescription(String link)
316 StringBuffer warnings = new StringBuffer();
317 if (!configureFromEncodedString(link, warnings))
319 if (warnings.length() > 0)
321 invalidMessage = warnings.toString();
326 public RestServiceDescription(RestServiceDescription toedit)
328 // Rather then do the above, we cheat and use our human readable
329 // serialization code to clone everything
330 this(toedit.toString());
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());
342 // TODO Implement copy constructor NOW*/
346 * @return the invalidMessage
348 public String getInvalidMessage()
350 return invalidMessage;
354 * Check if URL string was parsed properly.
356 * @return boolean - if false then <code>getInvalidMessage</code> returns an
359 public boolean isValid()
361 return invalidMessage == null;
365 * parse a string containing a list of service properties and configure the
366 * service description
369 * param warnings a StringBuffer that any warnings about invalid
370 * content will be appended to.
372 private boolean configureFromServiceInputProperties(String propList,
373 StringBuffer warnings)
375 String[] props = StringUtils.separatorListToArray(propList, ",");
381 boolean valid = true;
383 int l = warnings.length();
385 for (String prop : props)
387 if ((i = prop.indexOf("=")) > -1)
389 val = prop.substring(i + 1);
390 if (val.startsWith("\'") && val.endsWith("\'"))
392 val = val.substring(1, val.length() - 1);
394 prop = prop.substring(0, i);
397 if (prop.equals("hseparable"))
401 if (prop.equals("vseparable"))
405 if (prop.equals("gapCharacter"))
407 if (val == null || val.length() == 0 || val.length() > 1)
410 warnings.append((warnings.length() > 0 ? "\n" : "")
411 + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
416 gapCharacter = val.charAt(0);
419 if (prop.equals("returns"))
421 _configureOutputFormatFrom(val, warnings);
424 // return true if valid is true and warning buffer was not appended to.
425 return valid && (l == warnings.length());
428 private String _genOutputFormatString()
431 if (resultData == null)
435 for (JvDataType type : resultData)
437 if (buff.length() > 0)
441 buff += type.toString();
446 private void _configureOutputFormatFrom(String outstring,
447 StringBuffer warnings)
449 if (outstring.indexOf(";") == -1)
451 // we add a token, for simplicity
452 outstring = outstring + ";";
454 StringTokenizer st = new StringTokenizer(outstring, ";");
456 resultData = new ArrayList<JvDataType>();
457 while (st.hasMoreTokens())
461 resultData.add(JvDataType.valueOf(tok = st.nextToken()));
462 } catch (NoSuchElementException x)
464 warnings.append("Invalid result type: '" + tok
465 + "' (must be one of: ");
467 for (JvDataType vl : JvDataType.values())
469 warnings.append(sep);
470 warnings.append(vl.toString());
473 warnings.append(" separated by semi-colons)\n");
478 private String getServiceIOProperties()
480 ArrayList<String> vls = new ArrayList<String>();
483 vls.add("hseparable");
488 vls.add("vseparable");
491 vls.add(new String("gapCharacter='" + gapCharacter + "'"));
492 vls.add(new String("returns='" + _genOutputFormatString() + "'"));
493 return StringUtils.arrayToSeparatorList(vls.toArray(new String[0]), ",");
496 public String toString()
498 StringBuffer result = new StringBuffer();
500 result.append(details.Name);
502 result.append(details.Action);
504 if (details.description != null)
506 result.append(details.description);
509 // list job input flags
511 result.append(getServiceIOProperties());
512 // list any additional cgi parameters needed for result retrieval
513 if (urlSuffix != null && urlSuffix.length() > 0)
516 result.append(urlSuffix);
519 result.append(getInputParamEncodedUrl());
520 return result.toString();
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.
530 * - where warning messages are reported.
531 * @return true if configuration was parsed successfully.
533 public boolean configureFromEncodedString(String encoding,
534 StringBuffer warnings)
536 String[] list = StringUtils.separatorListToArray(encoding, "|");
538 int nextpos = parseServiceList(list, warnings, 0);
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.
558 protected int parseServiceList(String[] list, StringBuffer warnings, int p)
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)
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)
573 urlSuffix = list[p + 4];
574 invalid |= !configureFromInputParamEncodedUrl(list[p + 5], warnings);
579 if (list.length - p > 4 && list[p + 4] != null
580 && list[p + 4].trim().length() > 5)
583 invalid |= !configureFromInputParamEncodedUrl(list[p + 4], warnings);
587 return invalid ? -1 : p;
591 * @return string representation of the input parameters, their type and
592 * constraints, appended to the service's base submission URL
594 private String getInputParamEncodedUrl()
596 StringBuffer url = new StringBuffer();
597 if (postUrl == null || postUrl.length() < 5)
603 char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
604 boolean consts = true;
607 for (Map.Entry<String, InputType> param : inputParams.entrySet())
609 List<String> vals = param.getValue().getURLEncodedParameter();
610 if (param.getValue().isConstant())
614 url.append(appendChar);
616 url.append(param.getValue().token);
617 if (vals.size() == 1)
620 url.append(vals.get(0));
628 url.append(appendChar);
630 url.append(param.getValue().token);
632 // write parameter set as $TOKENPREFIX:csv list of params$ for this
635 url.append(param.getValue().getURLtokenPrefix());
637 url.append(StringUtils.arrayToSeparatorList(vals.toArray(new String[0]),
644 // toggle consts and repeat until !consts is false:
645 } while (!(consts = !consts));
646 return url.toString();
650 * parse the service URL and input parameters from the given encoded URL
651 * string and configure the RestServiceDescription from it.
656 * @return true if URL parsed correctly. false means the configuration failed.
658 private boolean configureFromInputParamEncodedUrl(String ipurl,
659 StringBuffer warnings)
661 boolean valid = true;
663 String url = new String();
664 Matcher prms = PARAM_ENCODED_URL_PATTERN
666 Map<String, InputType> iparams = new Hashtable<String, InputType>();
670 if (lastp < prms.start(0))
672 url += ipurl.substring(lastp, prms.start(0));
673 lastp = prms.end(0) + 1;
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 = "";
682 iprmparams = iprm.substring(colon + 1);
683 iprm = iprm.substring(0, colon);
685 valid = parseTypeString(prms.group(0), tok, iprm, iprmparams,
692 URL u = new URL(url);
694 inputParams = iparams;
695 } catch (Exception e)
697 warnings.append("Failed to parse '" + url + "' as a URL.\n");
704 public static Class[] getInputTypes()
706 // TODO - find a better way of maintaining this classlist
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 };
716 public static boolean parseTypeString(String fullstring, String tok,
717 String iprm, String iprmparams, Map<String, InputType> iparams,
718 StringBuffer warnings)
720 boolean valid = true;
722 for (Class type : getInputTypes())
726 jinput = (InputType) (type.getConstructor().newInstance());
727 if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
729 ArrayList<String> al = new ArrayList<String>();
730 for (String prprm : StringUtils.separatorListToArray(iprmparams, ","))
732 // hack to ensure that strings like "sep=','" containing unescaped
733 // commas as values are concatenated
734 al.add(prprm.trim());
736 if (!jinput.configureFromURLtokenString(al, warnings))
739 warnings.append("Failed to parse '" + fullstring + "' as a "
740 + jinput.getURLtokenPrefix() + " input tag.\n");
745 iparams.put(tok, jinput);
751 } catch (Throwable thr)
760 * covenience method to generate the id and sequence string vector from a set
761 * of seuqences using each sequence's getName() and getSequenceAsString()
765 * @return String[][] {{sequence ids},{sequence strings}}
767 public static String[][] formStrings(SequenceI[] seqs)
769 String[][] idset = new String[2][seqs.length];
770 for (int i = 0; i < seqs.length; i++)
772 idset[0][i] = seqs[i].getName();
773 idset[1][i] = seqs[i].getSequenceAsString();
779 * can this service be run on the visible portion of an alignment regardless
780 * of hidden boundaries ?
782 boolean hseparable = false;
784 boolean vseparable = false;
786 public boolean isHseparable()
795 public boolean isVseparable()
801 * search the input types for an instance of the given class
803 * @param <validInput.inputType> class1
806 public boolean inputInvolves(Class<?> class1)
808 assert (InputType.class.isAssignableFrom(class1));
809 for (InputType val : inputParams.values())
811 if (class1.isAssignableFrom(val.getClass()))
819 char gapCharacter = '-';
823 * @return the preferred gap character for alignments input/output by this
826 public char getGapCharacter()
831 public String getDecoratedResultUrl(String jobId)
833 // TODO: correctly write ?/& appropriate to result URL format.
834 return jobId + urlSuffix;
837 private List<JvDataType> resultData = new ArrayList<JvDataType>();
842 * TODO: Extend to optionally specify relative/absolute url where data of this
843 * type can be retrieved from
847 public void addResultDatatype(JvDataType dt)
849 if (resultData == null)
851 resultData = new ArrayList<JvDataType>();
856 public boolean removeRsultDatatype(JvDataType dt)
858 if (resultData != null)
860 return resultData.remove(dt);
865 public List<JvDataType> getResultDataTypes()
871 * parse a concatenated list of rest service descriptions into an array
874 * @return zero or more services.
876 * if the services are improperly encoded.
878 public static List<RestServiceDescription> parseDescriptions(
879 String services) throws Exception
881 String[] list = StringUtils.separatorListToArray(services, "|");
882 List<RestServiceDescription> svcparsed = new ArrayList<RestServiceDescription>();
883 int p = 0, lastp = 0;
884 StringBuffer warnings = new StringBuffer();
887 RestServiceDescription rsd = new RestServiceDescription();
888 p = rsd.parseServiceList(list, warnings, lastp = p);
889 if (p > lastp && rsd.isValid())
896 "Failed to parse user defined RSBS services from :"
898 + "\nFirst error was encountered at token " + lastp
899 + " starting " + list[lastp] + ":\n"
900 + rsd.getInvalidMessage());
902 } while (p < lastp && p < list.length - 1);