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;
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;
32 import java.util.ArrayList;
33 import java.util.HashMap;
34 import java.util.Hashtable;
35 import java.util.List;
37 import java.util.NoSuchElementException;
38 import java.util.StringTokenizer;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
42 public class RestServiceDescription
44 private static final Pattern PARAM_ENCODED_URL_PATTERN = Pattern
45 .compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$");
48 * create a new rest service description ready to be configured
50 public RestServiceDescription()
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)
70 this.details = new UIinfo(action, action, name, description, postUrl);
71 this.postUrl = postUrl == null ? "" : postUrl;
72 this.urlSuffix = urlSuffix == null ? "" : urlSuffix;
73 if (inputParams != null)
75 this.inputParams = inputParams;
77 this.hseparable = hseparable;
78 this.vseparable = vseparable;
79 this.gapCharacter = gapCharacter;
83 public boolean equals(Object o)
85 if (o == null || !(o instanceof RestServiceDescription))
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);
103 public UIinfo details;
105 public String getAction()
107 return details.getAction();
110 public void setAction(String action)
112 details.setAction(action);
115 public String getName()
117 return details.getName();
120 public void setName(String name)
122 details.setName(name);
125 public String getDescription()
127 return details.getDescription();
130 public void setDescription(String description)
132 details.setDescription(description);
140 public String getPostUrl()
145 public void setPostUrl(String postUrl)
147 this.postUrl = postUrl;
150 public String getUrlSuffix()
155 public void setUrlSuffix(String urlSuffix)
157 this.urlSuffix = urlSuffix;
160 public Map<String, InputType> getInputParams()
165 public void setInputParams(Map<String, InputType> inputParams)
167 this.inputParams = inputParams;
170 public void setHseparable(boolean hseparable)
172 this.hseparable = hseparable;
175 public void setVseparable(boolean vseparable)
177 this.vseparable = vseparable;
180 public void setGapCharacter(char gapCharacter)
182 this.gapCharacter = gapCharacter;
186 * suffix that should be added to any url used if it does not already end in
192 * input info given as key/value pairs - mapped to post arguments
194 Map<String, InputType> inputParams = new HashMap<>();
197 * assigns the given inputType it to its corresponding input parameter token
202 public void setInputParam(InputType it)
204 inputParams.put(it.token, it);
208 * remove the given input type it from the set of service input parameters.
212 public void removeInputParam(InputType it)
214 inputParams.remove(it.token);
218 * service requests alignment data
223 * service requests alignment and/or seuqence annotationo data
228 * service requests partitions defined over input (alignment) data
230 boolean partitiondata;
233 * process ths input data and set the appropriate shorthand flags describing
234 * the input the service wants
236 public void setInvolvesFlags()
238 aligndata = inputInvolves(Alignment.class);
239 annotdata = inputInvolves(AnnotationFile.class);
240 partitiondata = inputInvolves(SeqGroupIndexVector.class);
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
252 * Start with bare minimum: input is alignment + groups on alignment
258 private String invalidMessage = null;
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=>$.
269 public RestServiceDescription(String link)
271 StringBuffer warnings = new StringBuffer();
272 if (!configureFromEncodedString(link, warnings))
274 if (warnings.length() > 0)
276 invalidMessage = warnings.toString();
281 public RestServiceDescription(RestServiceDescription toedit)
283 // Rather then do the above, we cheat and use our human readable
284 // serialization code to clone everything
285 this(toedit.toString());
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());
297 // TODO Implement copy constructor NOW*/
301 * @return the invalidMessage
303 public String getInvalidMessage()
305 return invalidMessage;
309 * Check if URL string was parsed properly.
311 * @return boolean - if false then <code>getInvalidMessage</code> returns an
314 public boolean isValid()
316 return invalidMessage == null;
320 * parse a string containing a list of service properties and configure the
321 * service description
324 * param warnings a StringBuffer that any warnings about invalid
325 * content will be appended to.
327 private boolean configureFromServiceInputProperties(String propList,
328 StringBuffer warnings)
330 String[] props = StringUtils.separatorListToArray(propList, ",");
336 boolean valid = true;
338 int l = warnings.length();
340 for (String prop : props)
342 if ((i = prop.indexOf("=")) > -1)
344 val = prop.substring(i + 1);
345 if (val.startsWith("\'") && val.endsWith("\'"))
347 val = val.substring(1, val.length() - 1);
349 prop = prop.substring(0, i);
352 if (prop.equals("hseparable"))
356 if (prop.equals("vseparable"))
360 if (prop.equals("gapCharacter"))
362 if (val == null || val.length() == 0 || val.length() > 1)
365 warnings.append((warnings.length() > 0 ? "\n" : "")
366 + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
371 gapCharacter = val.charAt(0);
374 if (prop.equals("returns"))
376 _configureOutputFormatFrom(val, warnings);
379 // return true if valid is true and warning buffer was not appended to.
380 return valid && (l == warnings.length());
383 private String _genOutputFormatString()
386 if (resultData == null)
390 for (JvDataType type : resultData)
392 if (buff.length() > 0)
396 buff += type.toString();
401 private void _configureOutputFormatFrom(String outstring,
402 StringBuffer warnings)
404 if (outstring.indexOf(";") == -1)
406 // we add a token, for simplicity
407 outstring = outstring + ";";
409 StringTokenizer st = new StringTokenizer(outstring, ";");
411 resultData = new ArrayList<>();
412 while (st.hasMoreTokens())
416 resultData.add(JvDataType.valueOf(tok = st.nextToken()));
417 } catch (NoSuchElementException x)
420 "Invalid result type: '" + tok + "' (must be one of: ");
422 for (JvDataType vl : JvDataType.values())
424 warnings.append(sep);
425 warnings.append(vl.toString());
428 warnings.append(" separated by semi-colons)\n");
433 private String getServiceIOProperties()
435 ArrayList<String> vls = new ArrayList<>();
438 vls.add("hseparable");
443 vls.add("vseparable");
446 vls.add(new String("gapCharacter='" + gapCharacter + "'"));
447 vls.add(new String("returns='" + _genOutputFormatString() + "'"));
448 return StringUtils.arrayToSeparatorList(vls.toArray(new String[0]),
453 public String toString()
455 StringBuffer result = new StringBuffer();
457 result.append(details.getName());
459 result.append(details.getAction());
461 if (details.getDescription() != null)
463 result.append(details.getDescription());
466 // list job input flags
468 result.append(getServiceIOProperties());
469 // list any additional cgi parameters needed for result retrieval
470 if (urlSuffix != null && urlSuffix.length() > 0)
473 result.append(urlSuffix);
476 result.append(getInputParamEncodedUrl());
477 return result.toString();
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.
487 * - where warning messages are reported.
488 * @return true if configuration was parsed successfully.
490 public boolean configureFromEncodedString(String encoding,
491 StringBuffer warnings)
493 String[] list = StringUtils.separatorListToArray(encoding, "|");
495 int nextpos = parseServiceList(list, warnings, 0);
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.
515 protected int parseServiceList(String[] list, StringBuffer warnings,
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)
524 String action = list[p + 1], name = list[p], descrip = list[p + 2];
526 invalid |= !configureFromServiceInputProperties(list[p + 3], warnings);
527 if (list.length - p > 5 && list[p + 5] != null
528 && list[p + 5].trim().length() > 5)
530 urlSuffix = list[p + 4];
531 invalid |= !configureFromInputParamEncodedUrl(list[p + 5], warnings);
536 if (list.length - p > 4 && list[p + 4] != null
537 && list[p + 4].trim().length() > 5)
540 invalid |= !configureFromInputParamEncodedUrl(list[p + 4],
545 details = new UIinfo(action, action, name, descrip, postUrl);
546 return invalid ? -1 : p;
550 * @return string representation of the input parameters, their type and
551 * constraints, appended to the service's base submission URL
553 private String getInputParamEncodedUrl()
555 StringBuffer url = new StringBuffer();
556 if (postUrl == null || postUrl.length() < 5)
562 char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
563 boolean consts = true;
566 for (Map.Entry<String, InputType> param : inputParams.entrySet())
568 List<String> vals = param.getValue().getURLEncodedParameter();
569 if (param.getValue().isConstant())
573 url.append(appendChar);
575 url.append(param.getValue().token);
576 if (vals.size() == 1)
579 url.append(vals.get(0));
587 url.append(appendChar);
589 url.append(param.getValue().token);
591 // write parameter set as $TOKENPREFIX:csv list of params$ for this
594 url.append(param.getValue().getURLtokenPrefix());
596 url.append(StringUtils.arrayToSeparatorList(
597 vals.toArray(new String[0]), ","));
603 // toggle consts and repeat until !consts is false:
604 } while (!(consts = !consts));
605 return url.toString();
609 * parse the service URL and input parameters from the given encoded URL
610 * string and configure the RestServiceDescription from it.
615 * @return true if URL parsed correctly. false means the configuration failed.
617 private boolean configureFromInputParamEncodedUrl(String ipurl,
618 StringBuffer warnings)
620 boolean valid = true;
622 String url = new String();
623 Matcher prms = PARAM_ENCODED_URL_PATTERN.matcher(ipurl);
624 Map<String, InputType> iparams = new Hashtable<>();
628 if (lastp < prms.start(0))
630 url += ipurl.substring(lastp, prms.start(0));
631 lastp = prms.end(0) + 1;
633 String sep = prms.group(1);
634 String tok = prms.group(2);
635 String iprm = prms.group(3);
636 int colon = iprm.indexOf(":");
637 String iprmparams = "";
640 iprmparams = iprm.substring(colon + 1);
641 iprm = iprm.substring(0, colon);
643 valid = parseTypeString(prms.group(0), tok, iprm, iprmparams, iparams,
650 URL u = new URL(url);
652 inputParams = iparams;
653 } catch (Exception e)
655 warnings.append("Failed to parse '" + url + "' as a URL.\n");
662 public static Class[] getInputTypes()
664 // TODO - find a better way of maintaining this classlist
665 return new Class[] { jalview.ws.rest.params.Alignment.class,
666 jalview.ws.rest.params.AnnotationFile.class,
667 SeqGroupIndexVector.class, jalview.ws.rest.params.SeqIdVector.class,
668 jalview.ws.rest.params.SeqVector.class,
669 jalview.ws.rest.params.Tree.class };
672 public static boolean parseTypeString(String fullstring, String tok,
673 String iprm, String iprmparams, Map<String, InputType> iparams,
674 StringBuffer warnings)
676 boolean valid = true;
678 for (Class type : getInputTypes())
682 jinput = (InputType) (type.getConstructor().newInstance());
683 if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
685 ArrayList<String> al = new ArrayList<>();
686 for (String prprm : StringUtils.separatorListToArray(iprmparams,
689 // hack to ensure that strings like "sep=','" containing unescaped
690 // commas as values are concatenated
691 al.add(prprm.trim());
693 if (!jinput.configureFromURLtokenString(al, warnings))
696 warnings.append("Failed to parse '" + fullstring + "' as a "
697 + jinput.getURLtokenPrefix() + " input tag.\n");
702 iparams.put(tok, jinput);
708 } catch (Throwable thr)
717 * covenience method to generate the id and sequence string vector from a set
718 * of seuqences using each sequence's getName() and getSequenceAsString()
722 * @return String[][] {{sequence ids},{sequence strings}}
724 public static String[][] formStrings(SequenceI[] seqs)
726 String[][] idset = new String[2][seqs.length];
727 for (int i = 0; i < seqs.length; i++)
729 idset[0][i] = seqs[i].getName();
730 idset[1][i] = seqs[i].getSequenceAsString();
736 * can this service be run on the visible portion of an alignment regardless
737 * of hidden boundaries ?
739 boolean hseparable = false;
741 boolean vseparable = false;
743 public boolean isHseparable()
752 public boolean isVseparable()
758 * search the input types for an instance of the given class
760 * @param <validInput.inputType>
764 public boolean inputInvolves(Class<?> class1)
766 assert (InputType.class.isAssignableFrom(class1));
767 for (InputType val : inputParams.values())
769 if (class1.isAssignableFrom(val.getClass()))
777 char gapCharacter = '-';
781 * @return the preferred gap character for alignments input/output by this
784 public char getGapCharacter()
789 public String getDecoratedResultUrl(String jobId)
791 // TODO: correctly write ?/& appropriate to result URL format.
792 return jobId + urlSuffix;
795 private List<JvDataType> resultData = new ArrayList<>();
800 * TODO: Extend to optionally specify relative/absolute url where data of this
801 * type can be retrieved from
805 public void addResultDatatype(JvDataType dt)
807 if (resultData == null)
809 resultData = new ArrayList<>();
814 public boolean removeRsultDatatype(JvDataType dt)
816 if (resultData != null)
818 return resultData.remove(dt);
823 public List<JvDataType> getResultDataTypes()
829 * parse a concatenated list of rest service descriptions into an array
832 * @return zero or more services.
834 * if the services are improperly encoded.
836 public static List<RestServiceDescription> parseDescriptions(
837 String services) throws Exception
839 String[] list = StringUtils.separatorListToArray(services, "|");
840 List<RestServiceDescription> svcparsed = new ArrayList<>();
841 int p = 0, lastp = 0;
842 StringBuffer warnings = new StringBuffer();
845 RestServiceDescription rsd = new RestServiceDescription();
846 p = rsd.parseServiceList(list, warnings, lastp = p);
847 if (p > lastp && rsd.isValid())
854 "Failed to parse user defined RSBS services from :"
856 + "\nFirst error was encountered at token " + lastp
857 + " starting " + list[lastp] + ":\n"
858 + rsd.getInvalidMessage());
860 } while (p < lastp && p < list.length - 1);