2 * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
3 * Copyright (C) 2012 J Procter, AM Waterhouse, LM Lui, J Engelhardt, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
11 * Jalview is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
14 * PURPOSE. See the GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along with Jalview. If not, see <http://www.gnu.org/licenses/>.
18 package jalview.ws.rest;
20 import jalview.datamodel.SequenceI;
21 import jalview.io.packed.DataProvider.JvDataType;
22 import jalview.ws.rest.params.Alignment;
23 import jalview.ws.rest.params.AnnotationFile;
24 import jalview.ws.rest.params.SeqGroupIndexVector;
27 import java.util.ArrayList;
28 import java.util.HashMap;
29 import java.util.Hashtable;
30 import java.util.List;
32 import java.util.NoSuchElementException;
33 import java.util.StringTokenizer;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
37 public class RestServiceDescription
40 * create a new rest service description ready to be configured
42 public RestServiceDescription()
56 public RestServiceDescription(String action, String description,
57 String name, String postUrl, String urlSuffix,
58 Map<String, InputType> inputParams, boolean hseparable,
59 boolean vseparable, char gapCharacter)
62 this.details = new UIinfo();
63 details.Action = action == null ? "" : action;
64 details.description = description == null ? "" : description;
65 details.Name = name == null ? "" : name;
66 this.postUrl = postUrl == null ? "" : postUrl;
67 this.urlSuffix = urlSuffix == null ? "" : urlSuffix;
68 if (inputParams != null)
70 this.inputParams = inputParams;
72 this.hseparable = hseparable;
73 this.vseparable = vseparable;
74 this.gapCharacter = gapCharacter;
77 public boolean equals(Object o)
79 if (o == null || !(o instanceof RestServiceDescription))
83 RestServiceDescription other = (RestServiceDescription) o;
84 boolean diff = (gapCharacter != other.gapCharacter);
85 diff |= vseparable != other.vseparable;
86 diff |= hseparable != other.hseparable;
87 diff |= !(urlSuffix.equals(other.urlSuffix));
88 // TODO - robust diff that includes constants and reordering of URL
89 // diff |= !(postUrl.equals(other.postUrl));
90 // diff |= !inputParams.equals(other.inputParams);
91 diff |= !details.Name.equals(other.details.Name);
92 diff |= !details.Action.equals(other.details.Action);
93 diff |= !details.description.equals(other.details.description);
98 * Service UI Info { Action, Specific Name of Service, Brief Description }
103 public String getAction()
108 public void setAction(String action)
113 public String getName()
118 public void setName(String name)
123 public String getDescription()
128 public void setDescription(String description)
130 this.description = description;
140 public UIinfo details = new UIinfo();
142 public String getAction()
144 return details.getAction();
147 public void setAction(String action)
149 details.setAction(action);
152 public String getName()
154 return details.getName();
157 public void setName(String name)
159 details.setName(name);
162 public String getDescription()
164 return details.getDescription();
167 public void setDescription(String description)
169 details.setDescription(description);
177 public String getPostUrl()
182 public void setPostUrl(String postUrl)
184 this.postUrl = postUrl;
187 public String getUrlSuffix()
192 public void setUrlSuffix(String urlSuffix)
194 this.urlSuffix = urlSuffix;
197 public Map<String, InputType> getInputParams()
202 public void setInputParams(Map<String, InputType> inputParams)
204 this.inputParams = inputParams;
207 public void setHseparable(boolean hseparable)
209 this.hseparable = hseparable;
212 public void setVseparable(boolean vseparable)
214 this.vseparable = vseparable;
217 public void setGapCharacter(char gapCharacter)
219 this.gapCharacter = gapCharacter;
223 * suffix that should be added to any url used if it does not already end in
229 * input info given as key/value pairs - mapped to post arguments
231 Map<String, InputType> inputParams = new HashMap<String, InputType>();
234 * assigns the given inputType it to its corresponding input parameter token
239 public void setInputParam(InputType it)
241 inputParams.put(it.token, it);
245 * remove the given input type it from the set of service input parameters.
249 public void removeInputParam(InputType it)
251 inputParams.remove(it.token);
255 * service requests alignment data
260 * service requests alignment and/or seuqence annotationo data
265 * service requests partitions defined over input (alignment) data
267 boolean partitiondata;
270 * process ths input data and set the appropriate shorthand flags describing
271 * the input the service wants
273 public void setInvolvesFlags()
275 aligndata = inputInvolves(Alignment.class);
276 annotdata = inputInvolves(AnnotationFile.class);
277 partitiondata = inputInvolves(SeqGroupIndexVector.class);
281 * Service return info { alignment, annotation file (loaded back on to
282 * alignment), tree (loaded back on to alignment), sequence annotation -
283 * loaded back on to alignment), text report, pdb structures with sequence
289 * Start with bare minimum: input is alignment + groups on alignment
295 private String invalidMessage = null;
298 * parse the given linkString of the form '<label>|<url>|separator
299 * char[|optional sequence separator char]' into parts. url may contain a
300 * string $SEQUENCEIDS<=optional regex=>$ where <=optional regex=> must be of
301 * the form =/<perl style regex>/=$ or $SEQUENCES<=optional regex=>$ or
302 * $SEQUENCES<=optional regex=>$.
306 public RestServiceDescription(String link)
308 StringBuffer warnings = new StringBuffer();
309 if (!configureFromEncodedString(link, warnings))
311 if (warnings.length() > 0)
313 invalidMessage = warnings.toString();
318 public RestServiceDescription(RestServiceDescription toedit)
320 // Rather then do the above, we cheat and use our human readable
321 // serialization code to clone everything
322 this(toedit.toString());
324 * if (toedit == null) { return; } /** urlSuffix = toedit.urlSuffix; postUrl
325 * = toedit.postUrl; hseparable = toedit.hseparable; vseparable =
326 * toedit.vseparable; gapCharacter = toedit.gapCharacter; details = new
327 * RestServiceDescription.UIinfo(); details.Action = toedit.details.Action;
328 * details.description = toedit.details.description; details.Name =
329 * toedit.details.Name; for (InputType itype: toedit.inputParams.values()) {
330 * inputParams.put(itype.token, itype.clone());
334 // TODO Implement copy constructor NOW*/
338 * @return the invalidMessage
340 public String getInvalidMessage()
342 return invalidMessage;
346 * Check if URL string was parsed properly.
348 * @return boolean - if false then <code>getInvalidMessage</code> returns an
351 public boolean isValid()
353 return invalidMessage == null;
356 private static boolean debug = false;
359 * parse the string into a list
363 * @return elements separated by separator
365 public static String[] separatorListToArray(String list, String separator)
367 int seplen = separator.length();
368 if (list == null || list.equals("") || list.equals(separator))
370 java.util.ArrayList<String> jv = new ArrayList<String>();
371 int cp = 0, pos, escape;
372 boolean wasescaped = false, wasquoted = false;
373 String lstitem = null;
374 while ((pos = list.indexOf(separator, cp)) >= cp)
377 escape = (pos > 0 && list.charAt(pos - 1) == '\\') ? -1 : 0;
378 if (wasescaped || wasquoted)
380 // append to previous pos
381 jv.set(jv.size() - 1,
382 lstitem = lstitem + separator
383 + list.substring(cp, pos + escape));
388 jv.add(lstitem = list.substring(cp, pos + escape));
391 wasescaped = escape == -1;
394 // last separator may be in an unmatched quote
395 if (java.util.regex.Pattern.matches("('[^']*')*[^']*'", lstitem))
402 if (cp < list.length())
404 String c = list.substring(cp);
405 if (wasescaped || wasquoted)
407 // append final separator
408 jv.set(jv.size() - 1, lstitem + separator + c);
412 if (!c.equals(separator))
420 String[] v = jv.toArray(new String[jv.size()]);
424 System.err.println("Array from '" + separator
425 + "' separated List:\n" + v.length);
426 for (int i = 0; i < v.length; i++)
428 System.err.println("item " + i + " '" + v[i] + "'");
435 System.err.println("Empty Array from '" + separator
436 + "' separated List");
442 * concatenate the list with separator
446 * @return concatenated string
448 public static String arrayToSeparatorList(String[] list, String separator)
450 StringBuffer v = new StringBuffer();
451 if (list != null && list.length > 0)
453 for (int i = 0, iSize = list.length; i < iSize; i++)
461 // TODO - escape any separator values in list[i]
467 System.err.println("Returning '" + separator
468 + "' separated List:\n");
469 System.err.println(v);
475 System.err.println("Returning empty '" + separator
476 + "' separated List\n");
478 return "" + separator;
482 * parse a string containing a list of service properties and configure the
483 * service description
486 * param warnings a StringBuffer that any warnings about invalid
487 * content will be appended to.
489 private boolean configureFromServiceInputProperties(String propList,
490 StringBuffer warnings)
492 String[] props = separatorListToArray(propList, ",");
498 boolean valid = true;
500 int l = warnings.length();
502 for (String prop : props)
504 if ((i = prop.indexOf("=")) > -1)
506 val = prop.substring(i + 1);
507 if (val.startsWith("\'") && val.endsWith("\'"))
509 val = val.substring(1, val.length() - 1);
511 prop = prop.substring(0, i);
514 if (prop.equals("hseparable"))
518 if (prop.equals("vseparable"))
522 if (prop.equals("gapCharacter"))
524 if (val == null || val.length() == 0 || val.length() > 1)
527 warnings.append((warnings.length() > 0 ? "\n" : "")
528 + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
533 gapCharacter = val.charAt(0);
536 if (prop.equals("returns"))
538 _configureOutputFormatFrom(val, warnings);
541 // return true if valid is true and warning buffer was not appended to.
542 return valid && (l == warnings.length());
545 private String _genOutputFormatString()
548 if (resultData == null)
552 for (JvDataType type : resultData)
554 if (buff.length() > 0)
558 buff += type.toString();
563 private void _configureOutputFormatFrom(String outstring,
564 StringBuffer warnings)
566 if (outstring.indexOf(";") == -1)
568 // we add a token, for simplicity
569 outstring = outstring + ";";
571 StringTokenizer st = new StringTokenizer(outstring, ";");
573 resultData = new ArrayList<JvDataType>();
574 while (st.hasMoreTokens())
578 resultData.add(JvDataType.valueOf(tok = st.nextToken()));
579 } catch (NoSuchElementException x)
581 warnings.append("Invalid result type: '" + tok
582 + "' (must be one of: ");
584 for (JvDataType vl : JvDataType.values())
586 warnings.append(sep);
587 warnings.append(vl.toString());
590 warnings.append(" separated by semi-colons)\n");
595 private String getServiceIOProperties()
597 ArrayList<String> vls = new ArrayList<String>();
600 vls.add("hseparable");
605 vls.add("vseparable");
608 vls.add(new String("gapCharacter='" + gapCharacter + "'"));
609 vls.add(new String("returns='" + _genOutputFormatString() + "'"));
610 return arrayToSeparatorList(vls.toArray(new String[0]), ",");
613 public String toString()
615 StringBuffer result = new StringBuffer();
617 result.append(details.Name);
619 result.append(details.Action);
621 if (details.description != null)
623 result.append(details.description);
626 // list job input flags
628 result.append(getServiceIOProperties());
629 // list any additional cgi parameters needed for result retrieval
630 if (urlSuffix != null && urlSuffix.length() > 0)
633 result.append(urlSuffix);
636 result.append(getInputParamEncodedUrl());
637 return result.toString();
641 * processes a service encoded as a string (as generated by
642 * RestServiceDescription.toString()) Note - this will only use the first
643 * service definition encountered in the string to configure the service.
647 * - where warning messages are reported.
648 * @return true if configuration was parsed successfully.
650 public boolean configureFromEncodedString(String encoding,
651 StringBuffer warnings)
653 String[] list = separatorListToArray(encoding, "|");
655 int nextpos = parseServiceList(list, warnings, 0);
664 * processes the given list from position p, attempting to configure the
665 * service from it. Service lists are formed by concatenating individual
666 * stringified services. The first character of a stringified service is '|',
667 * enabling this, and the parser will ignore empty fields in a '|' separated
668 * list when they fall outside a service definition.
675 protected int parseServiceList(String[] list, StringBuffer warnings, int p)
677 boolean invalid = false;
678 // look for the first non-empty position - expect it to be service name
679 while (list[p] != null && list[p].trim().length() == 0)
683 details.Name = list[p];
684 details.Action = list[p + 1];
685 details.description = list[p + 2];
686 invalid |= !configureFromServiceInputProperties(list[p + 3], warnings);
687 if (list.length - p > 5 && list[p + 5] != null
688 && list[p + 5].trim().length() > 5)
690 urlSuffix = list[p + 4];
691 invalid |= !configureFromInputParamEncodedUrl(list[p + 5], warnings);
696 if (list.length - p > 4 && list[p + 4] != null
697 && list[p + 4].trim().length() > 5)
700 invalid |= !configureFromInputParamEncodedUrl(list[p + 4], warnings);
704 return invalid ? -1 : p;
708 * @return string representation of the input parameters, their type and
709 * constraints, appended to the service's base submission URL
711 private String getInputParamEncodedUrl()
713 StringBuffer url = new StringBuffer();
714 if (postUrl == null || postUrl.length() < 5)
720 char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
721 boolean consts = true;
724 for (Map.Entry<String, InputType> param : inputParams.entrySet())
726 List<String> vals = param.getValue().getURLEncodedParameter();
727 if (param.getValue().isConstant())
731 url.append(appendChar);
733 url.append(param.getValue().token);
734 if (vals.size() == 1)
737 url.append(vals.get(0));
745 url.append(appendChar);
747 url.append(param.getValue().token);
749 // write parameter set as $TOKENPREFIX:csv list of params$ for this
752 url.append(param.getValue().getURLtokenPrefix());
754 url.append(arrayToSeparatorList(vals.toArray(new String[0]),
761 // toggle consts and repeat until !consts is false:
762 } while (!(consts = !consts));
763 return url.toString();
767 * parse the service URL and input parameters from the given encoded URL
768 * string and configure the RestServiceDescription from it.
773 * @return true if URL parsed correctly. false means the configuration failed.
775 private boolean configureFromInputParamEncodedUrl(String ipurl,
776 StringBuffer warnings)
778 boolean valid = true;
780 String url = new String();
781 Matcher prms = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$")
783 Map<String, InputType> iparams = new Hashtable<String, InputType>();
787 if (lastp < prms.start(0))
789 url += ipurl.substring(lastp, prms.start(0));
790 lastp = prms.end(0) + 1;
792 String sep = prms.group(1);
793 String tok = prms.group(2);
794 String iprm = prms.group(3);
795 int colon = iprm.indexOf(":");
796 String iprmparams = "";
799 iprmparams = iprm.substring(colon + 1);
800 iprm = iprm.substring(0, colon);
802 valid = parseTypeString(prms.group(0), tok, iprm, iprmparams,
809 URL u = new URL(url);
811 inputParams = iparams;
812 } catch (Exception e)
814 warnings.append("Failed to parse '" + url + "' as a URL.\n");
821 public static Class[] getInputTypes()
823 // TODO - find a better way of maintaining this classlist
825 { jalview.ws.rest.params.Alignment.class,
826 jalview.ws.rest.params.AnnotationFile.class,
827 SeqGroupIndexVector.class,
828 jalview.ws.rest.params.SeqIdVector.class,
829 jalview.ws.rest.params.SeqVector.class,
830 jalview.ws.rest.params.Tree.class };
833 public static boolean parseTypeString(String fullstring, String tok,
834 String iprm, String iprmparams, Map<String, InputType> iparams,
835 StringBuffer warnings)
837 boolean valid = true;
839 for (Class type : getInputTypes())
843 jinput = (InputType) (type.getConstructor().newInstance(null));
844 if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
846 ArrayList<String> al = new ArrayList<String>();
847 for (String prprm : separatorListToArray(iprmparams, ","))
849 // hack to ensure that strings like "sep=','" containing unescaped
850 // commas as values are concatenated
851 al.add(prprm.trim());
853 if (!jinput.configureFromURLtokenString(al, warnings))
856 warnings.append("Failed to parse '" + fullstring + "' as a "
857 + jinput.getURLtokenPrefix() + " input tag.\n");
862 iparams.put(tok, jinput);
868 } catch (Throwable thr)
877 * covenience method to generate the id and sequence string vector from a set
878 * of seuqences using each sequence's getName() and getSequenceAsString()
882 * @return String[][] {{sequence ids},{sequence strings}}
884 public static String[][] formStrings(SequenceI[] seqs)
886 String[][] idset = new String[2][seqs.length];
887 for (int i = 0; i < seqs.length; i++)
889 idset[0][i] = seqs[i].getName();
890 idset[1][i] = seqs[i].getSequenceAsString();
896 * can this service be run on the visible portion of an alignment regardless
897 * of hidden boundaries ?
899 boolean hseparable = false;
901 boolean vseparable = false;
903 public boolean isHseparable()
912 public boolean isVseparable()
918 * search the input types for an instance of the given class
920 * @param <validInput.inputType> class1
923 public boolean inputInvolves(Class<?> class1)
925 assert (InputType.class.isAssignableFrom(class1));
926 for (InputType val : inputParams.values())
928 if (class1.isAssignableFrom(val.getClass()))
936 char gapCharacter = '-';
940 * @return the preferred gap character for alignments input/output by this
943 public char getGapCharacter()
948 public String getDecoratedResultUrl(String jobId)
950 // TODO: correctly write ?/& appropriate to result URL format.
951 return jobId + urlSuffix;
954 private List<JvDataType> resultData = new ArrayList<JvDataType>();
959 * TODO: Extend to optionally specify relative/absolute url where data of this
960 * type can be retrieved from
964 public void addResultDatatype(JvDataType dt)
966 if (resultData == null)
968 resultData = new ArrayList<JvDataType>();
973 public boolean removeRsultDatatype(JvDataType dt)
975 if (resultData != null)
977 return resultData.remove(dt);
982 public List<JvDataType> getResultDataTypes()
988 * parse a concatenated list of rest service descriptions into an array
991 * @return zero or more services.
993 * if the services are improperly encoded.
995 public static List<RestServiceDescription> parseDescriptions(
996 String services) throws Exception
998 String[] list = separatorListToArray(services, "|");
999 List<RestServiceDescription> svcparsed = new ArrayList<RestServiceDescription>();
1000 int p = 0, lastp = 0;
1001 StringBuffer warnings = new StringBuffer();
1004 RestServiceDescription rsd = new RestServiceDescription();
1005 p = rsd.parseServiceList(list, warnings, lastp = p);
1006 if (p > lastp && rsd.isValid())
1012 throw new Exception(
1013 "Failed to parse user defined RSBS services from :"
1015 + "\nFirst error was encountered at token " + lastp
1016 + " starting " + list[lastp] + ":\n"
1017 + rsd.getInvalidMessage());
1019 } while (p < lastp && p < list.length - 1);