JAL-715 - simple tests and functions for IO of service as '|' string
authorjprocter <jprocter@compbio.dundee.ac.uk>
Tue, 23 Aug 2011 12:30:22 +0000 (13:30 +0100)
committerjprocter <jprocter@compbio.dundee.ac.uk>
Tue, 23 Aug 2011 12:30:22 +0000 (13:30 +0100)
src/jalview/ws/rest/RestServiceDescription.java

index 17d7a6e..dea7fc9 100644 (file)
@@ -17,7 +17,6 @@
  */
 package jalview.ws.rest;
 
-
 import jalview.datamodel.SequenceI;
 import jalview.io.packed.DataProvider;
 import jalview.io.packed.SimpleDataProvider;
@@ -26,14 +25,26 @@ import jalview.util.GroupUrlLink.UrlStringTooLongException;
 import jalview.util.Platform;
 import jalview.ws.rest.params.Alignment;
 import jalview.ws.rest.params.AnnotationFile;
+import jalview.ws.rest.params.JobConstant;
 import jalview.ws.rest.params.SeqGroupIndexVector;
 
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+import java.util.Vector;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.JViewport;
 
+import com.stevesoft.pat.Regex;
+import com.sun.org.apache.xml.internal.serialize.OutputFormat.DTD;
+import com.sun.tools.doclets.internal.toolkit.util.DocFinder.Output;
 
 public class RestServiceDescription
 {
@@ -46,13 +57,14 @@ public class RestServiceDescription
    * @param vseparable
    * @param gapCharacter
    */
-  public RestServiceDescription(String action,String description,String name, String postUrl,
-          String urlSuffix, Map<String, InputType> inputParams,
-          boolean hseparable, boolean vseparable, char gapCharacter)
+  public RestServiceDescription(String action, String description,
+          String name, String postUrl, String urlSuffix,
+          Map<String, InputType> inputParams, boolean hseparable,
+          boolean vseparable, char gapCharacter)
   {
     super();
     this.details = new UIinfo();
-    details.Action= action;
+    details.Action = action;
     details.description = description;
     details.Name = name;
     this.postUrl = postUrl;
@@ -62,788 +74,678 @@ public class RestServiceDescription
     this.vseparable = vseparable;
     this.gapCharacter = gapCharacter;
   }
+
+  public boolean equals(Object o)
+  {
+    if (o == null || !(o instanceof RestServiceDescription))
+    {
+      return false;
+    }
+    RestServiceDescription other = (RestServiceDescription) o;
+    boolean diff = (gapCharacter != other.gapCharacter);
+    diff |= vseparable != other.vseparable;
+    diff |= hseparable != other.hseparable;
+    diff |= !(urlSuffix.equals(other.urlSuffix));
+    // TODO - robust diff that includes constants and reordering of URL
+    // diff |= !(postUrl.equals(other.postUrl));
+    // diff |= !inputParams.equals(other.inputParams);
+    diff |= !details.Name.equals(other.details.Name);
+    diff |= !details.Action.equals(other.details.Action);
+    diff |= !details.description.equals(other.details.description);
+    return !diff;
+  }
+
   /**
    * Service UI Info { Action, Specific Name of Service, Brief Description }
    */
-   
-  public class UIinfo {
+
+  public class UIinfo
+  {
     String Action;
+
     String Name;
+
     String description;
   }
+
   public UIinfo details = new UIinfo();
-  
-  /** Service base URL
+
+  /**
+   * Service base URL
    */
   String postUrl;
+
   /**
-   * suffix that should be added to any url used if it does not already end in the suffix.
+   * suffix that should be added to any url used if it does not already end in
+   * the suffix.
    */
   String urlSuffix;
-  
-  /** input info given as key/value pairs - mapped to post arguments 
-   */ 
-  Map<String,InputType> inputParams=new HashMap();
+
   /**
-   * assigns the given inputType it to its corresponding input parameter token it.token  
+   * input info given as key/value pairs - mapped to post arguments
+   */
+  Map<String, InputType> inputParams = new HashMap();
+
+  /**
+   * assigns the given inputType it to its corresponding input parameter token
+   * it.token
+   * 
    * @param it
    */
   public void setInputParam(InputType it)
   {
     inputParams.put(it.token, it);
   }
+
   /**
    * remove the given input type it from the set of service input parameters.
+   * 
    * @param it
    */
   public void removeInputParam(InputType it)
   {
     inputParams.remove(it.token);
   }
+
   /**
    * service requests alignment data
    */
   boolean aligndata;
+
   /**
-   * service requests alignment and/or seuqence annotationo data 
+   * service requests alignment and/or seuqence annotationo data
    */
   boolean annotdata;
+
   /**
    * service requests partitions defined over input (alignment) data
    */
   boolean partitiondata;
-  
+
   /**
-   * process ths input data and set the appropriate shorthand flags describing the input the service wants
+   * process ths input data and set the appropriate shorthand flags describing
+   * the input the service wants
    */
-  public void setInvolvesFlags() {
+  public void setInvolvesFlags()
+  {
     aligndata = inputInvolves(Alignment.class);
     annotdata = inputInvolves(AnnotationFile.class);
     partitiondata = inputInvolves(SeqGroupIndexVector.class);
   }
 
-  /** Service return info { alignment, annotation file (loaded back on to alignment), tree (loaded back on to alignment), sequence annotation - loaded back on to alignment), text report, pdb structures with sequence mapping )
-   * 
-   */ 
-  
-  /** Start with bare minimum: input is alignment + groups on alignment
-   *    
-   * @author JimP
-   *
-   */
-
   /**
-   * Helper class based on the UrlLink class which enables URLs to be
-   * constructed from sequences or IDs associated with a group of sequences. URL
-   * definitions consist of a pipe separated string containing a <label>|<url
-   * construct>|<separator character>[|<sequence separator character>]. The url
-   * construct includes regex qualified tokens which are replaced with seuqence
-   * IDs ($SEQUENCE_IDS$) and/or seuqence regions ($SEQUENCES$) that are
-   * extracted from the group. See <code>UrlLink</code> for more information
-   * about the approach, and the original implementation. Documentation to come.
-   * Note - groupUrls can be very big!
+   * Service return info { alignment, annotation file (loaded back on to
+   * alignment), tree (loaded back on to alignment), sequence annotation -
+   * loaded back on to alignment), text report, pdb structures with sequence
+   * mapping )
+   * 
    */
-  private String url_prefix, target, label;
 
   /**
-   * these are all filled in order of the occurence of each token in the url
-   * string template
+   * Start with bare minimum: input is alignment + groups on alignment
+   * 
+   * @author JimP
+   * 
    */
-  private String url_suffix[], separators[], regexReplace[];
 
   private String invalidMessage = null;
 
   /**
-   * tokens that can be replaced in the URL.
-   */
-  private static String[] tokens;
-
-  /**
-   * position of each token (which can appear once only) in the url
-   */
-  private int[] segs;
-
-  /**
-   * contains tokens in the order they appear in the URL template.
+   * parse the given linkString of the form '<label>|<url>|separator
+   * char[|optional sequence separator char]' into parts. url may contain a
+   * string $SEQUENCEIDS<=optional regex=>$ where <=optional regex=> must be of
+   * the form =/<perl style regex>/=$ or $SEQUENCES<=optional regex=>$ or
+   * $SEQUENCES<=optional regex=>$.
+   * 
+   * @param link
    */
-  private String[] mtch;
-  static
+  public RestServiceDescription(String link)
   {
-    if (tokens == null)
+    StringBuffer warnings = new StringBuffer();
+    if (!configureFromEncodedString(link, warnings))
     {
-      tokens = new String[]
-      { "SEQUENCEIDS", "SEQUENCES", "DATASETID" };
+      if (warnings.length() > 0)
+      {
+        invalidMessage = warnings.toString();
+      }
     }
   }
 
-  /**
-   * test for GroupURLType bitfield (with default tokens)
-   */
-  public static final int SEQUENCEIDS = 1;
+  public RestServiceDescription(RestServiceDescription toedit)
+  {
+    if (toedit == null)
+    {
+      return;
+    }
+    // TODO Implement copy constructor NOW
+  }
 
   /**
-   * test for GroupURLType bitfield (with default tokens)
+   * @return the invalidMessage
    */
-  public static final int SEQUENCES = 2;
+  public String getInvalidMessage()
+  {
+    return invalidMessage;
+  }
 
   /**
-   * test for GroupURLType bitfield (with default tokens)
+   * Check if URL string was parsed properly.
+   * 
+   * @return boolean - if false then <code>getInvalidMessage</code> returns an
+   *         error message
    */
-  public static final int DATASETID = 4;
+  public boolean isValid()
+  {
+    return invalidMessage == null;
+  }
 
-  // private int idseg = -1, seqseg = -1;
+  private static boolean debug = false;
 
   /**
-   * parse the given linkString of the form '<label>|<url>|separator
-   * char[|optional sequence separator char]' into parts. url may contain a
-   * string $SEQUENCEIDS<=optional regex=>$ where <=optional regex=> must be of
-   * the form =/<perl style regex>/=$ or $SEQUENCES<=optional regex=>$ or
-   * $SEQUENCES<=optional regex=>$.
+   * parse the string into a list
    * 
-   * @param link
+   * @param list
+   * @param separator
+   * @return elements separated by separator
    */
-  public RestServiceDescription(String link)
+  public static String[] separatorListToArray(String list, String separator)
   {
-    int sep = link.indexOf("|");
-    segs = new int[tokens.length];
-    int ntoks = 0;
-    for (int i = 0; i < segs.length; i++)
+    int seplen = separator.length();
+    if (list == null || list.equals("") || list.equals(separator))
+      return null;
+    java.util.ArrayList<String> jv = new ArrayList<String>();
+    int cp = 0, pos, escape;
+    boolean wasescaped = false;
+    String lstitem = null;
+    while ((pos = list.indexOf(separator, cp)) > cp)
     {
-      if ((segs[i] = link.indexOf("$" + tokens[i])) > -1)
+      escape = (list.charAt(pos - 1) == '\\') ? -1 : 0;
+      if (wasescaped)
       {
-        ntoks++;
+        // append to previous pos
+        jv.set(jv.size() - 1,
+                lstitem = lstitem + separator
+                        + list.substring(cp, pos + escape));
+
       }
+      else
+      {
+        jv.add(lstitem = list.substring(cp, pos + escape));
+      }
+      cp = pos + seplen;
+      wasescaped = escape == -1;
     }
-    // expect at least one token
-    if (ntoks == 0)
+    if (cp < list.length())
     {
-      invalidMessage = "Group URL string must contain at least one of ";
-      for (int i = 0; i < segs.length; i++)
+      String c = list.substring(cp);
+      if (wasescaped)
       {
-        invalidMessage += " '$" + tokens[i] + "[=/regex=/]$'";
+        // append final separator
+        jv.set(jv.size() - 1, lstitem + separator + c);
+      }
+      else
+      {
+        if (!c.equals(separator))
+        {
+          jv.add(c);
+        }
       }
-      return;
     }
-
-    int[] ptok = new int[ntoks + 1];
-    String[] tmtch = new String[ntoks + 1];
-    mtch = new String[ntoks];
-    for (int i = 0, t = 0; i < segs.length; i++)
+    if (jv.size() > 0)
     {
-      if (segs[i] > -1)
+      String[] v = jv.toArray(new String[jv.size()]);
+      jv.clear();
+      if (debug)
       {
-        ptok[t] = segs[i];
-        tmtch[t++] = tokens[i];
+        System.err.println("Array from '" + separator
+                + "' separated List:\n" + v.length);
+        for (int i = 0; i < v.length; i++)
+        {
+          System.err.println("item " + i + " '" + v[i] + "'");
+        }
       }
+      return v;
     }
-    ptok[ntoks] = link.length();
-    tmtch[ntoks] = "$$$$$$$$$";
-    jalview.util.QuickSort.sort(ptok, tmtch);
-    for (int i = 0; i < ntoks; i++)
+    if (debug)
     {
-      mtch[i] = tmtch[i]; // TODO: check order is ascending
+      System.err.println("Empty Array from '" + separator
+              + "' separated List");
     }
-    /*
-     * replaces the specific code below {}; if (psqids > -1 && pseqs > -1) { if
-     * (psqids > pseqs) { idseg = 1; seqseg = 0;
-     * 
-     * ptok = new int[] { pseqs, psqids, link.length() }; mtch = new String[] {
-     * "$SEQUENCES", "$SEQUENCEIDS" }; } else { idseg = 0; seqseg = 1; ptok =
-     * new int[] { psqids, pseqs, link.length() }; mtch = new String[] {
-     * "$SEQUENCEIDS", "$SEQUENCES" }; } } else { if (psqids != -1) { idseg = 0;
-     * ptok = new int[] { psqids, link.length() }; mtch = new String[] {
-     * "$SEQUENCEIDS" }; } else { seqseg = 0; ptok = new int[] { pseqs,
-     * link.length() }; mtch = new String[] { "$SEQUENCES" }; } }
-     */
-
-    int p = sep;
-    // first get the label and target part before the first |
-    do
-    {
-      sep = p;
-      p = link.indexOf("|", sep + 1);
-    } while (p > sep && p < ptok[0]);
-    // Assuming that the URL itself does not contain any '|' symbols
-    // sep now contains last pipe symbol position prior to any regex symbols
-    label = link.substring(0, sep);
-    if (label.indexOf("|") > -1)
+    return null;
+  }
+
+  /**
+   * concatenate the list with separator
+   * 
+   * @param list
+   * @param separator
+   * @return concatenated string
+   */
+  public static String arrayToSeparatorList(String[] list, String separator)
+  {
+    StringBuffer v = new StringBuffer();
+    if (list != null && list.length > 0)
     {
-      // | terminated database name / www target at start of Label
-      target = label.substring(0, label.indexOf("|"));
+      for (int i = 0, iSize = list.length; i < iSize; i++)
+      {
+        if (list[i] != null)
+        {
+          if (i > 0)
+          {
+            v.append(separator);
+          }
+          // TODO - escape any separator values in list[i]
+          v.append(list[i]);
+        }
+      }
+      if (debug)
+      {
+        System.err.println("Returning '" + separator
+                + "' separated List:\n");
+        System.err.println(v);
+      }
+      return v.toString();
     }
-    else if (label.indexOf(" ") > 2)
+    if (debug)
     {
-      // space separated Label - matches database name
-      target = label.substring(0, label.indexOf(" "));
+      System.err.println("Returning empty '" + separator
+              + "' separated List\n");
     }
-    else
+    return "" + separator;
+  }
+
+  /**
+   * parse a string containing a list of service properties and configure the
+   * service description
+   * 
+   * @param propList
+   *          param warnings a StringBuffer that any warnings about invalid
+   *          content will be appended to.
+   */
+  private void configureFromServiceInputProperties(String propList,
+          StringBuffer warnings)
+  {
+    String[] props = separatorListToArray(propList, ",");
+    if (props == null)
     {
-      target = label;
+      return;
     }
-    // Now Parse URL : Whole URL string first
-    url_prefix = link.substring(sep + 1, ptok[0]);
-    url_suffix = new String[mtch.length];
-    regexReplace = new String[mtch.length];
-    // and loop through tokens
-    for (int pass = 0; pass < mtch.length; pass++)
-    {
-      int mlength = 3 + mtch[pass].length();
-      if (link.indexOf("$" + mtch[pass] + "=/") == ptok[pass]
-              && (p = link.indexOf("/=$", ptok[pass] + mlength)) > ptok[pass]
-                      + mlength)
+    ;
+    String val = null;
+    int i;
+    for (String prop : props)
+    {
+      if ((i = prop.indexOf("=")) > -1)
       {
-        // Extract Regex and suffix
-        if (ptok[pass + 1] < p + 3)
+        val = prop.substring(i + 1);
+        if (val.startsWith("\'") && val.endsWith("\'"))
         {
-          // tokens are not allowed inside other tokens - e.g. inserting a
-          // $sequences$ into the regex match for the sequenceid
-          invalidMessage = "Token regexes cannot contain other regexes (did you terminate the $"
-                  + mtch[pass] + " regex with a '/=$' ?";
-          return;
-        }
-        url_suffix[pass] = link.substring(p + 3, ptok[pass + 1]);
-        regexReplace[pass] = link.substring(ptok[pass] + mlength, p);
-        try
-        {
-          com.stevesoft.pat.Regex rg = com.stevesoft.pat.Regex.perlCode("/"
-                  + regexReplace[pass] + "/");
-          if (rg == null)
-          {
-            invalidMessage = "Invalid Regular Expression : '"
-                    + regexReplace[pass] + "'\n";
-          }
-        } catch (Exception e)
-        {
-          invalidMessage = "Invalid Regular Expression : '"
-                  + regexReplace[pass] + "'\n";
+          val = val.substring(1, val.length() - 1);
         }
+        prop = prop.substring(0, i);
       }
-      else
+
+      if (prop.equals("hseparable"))
+      {
+        hseparable = true;
+      }
+      if (prop.equals("vseparable"))
+      {
+        vseparable = true;
+      }
+      if (prop.equals("gapCharacter"))
       {
-        regexReplace[pass] = null;
-        // verify format is really correct.
-        if ((p = link.indexOf("$" + mtch[pass] + "$")) == ptok[pass])
+        if (val == null || val.length() > 1)
         {
-          url_suffix[pass] = link.substring(p + mtch[pass].length() + 2,
-                  ptok[pass + 1]);
+          warnings.append((warnings.length() > 0 ? "\n" : "")
+                  + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
+                          + val + "'"));
         }
         else
         {
-          invalidMessage = "Warning: invalid regex structure (after '"
-                  + mtch[0] + "') for URL link : " + link;
+          gapCharacter = val.charAt(0);
         }
       }
-    }
-    int pass = 0;
-    separators = new String[url_suffix.length];
-    String suffices = url_suffix[url_suffix.length - 1], lastsep = ",";
-    // have a look in the last suffix for any more separators.
-    while ((p = suffices.indexOf('|')) > -1)
-    {
-      separators[pass] = suffices.substring(p + 1);
-      if (pass == 0)
-      {
-        // trim the original suffix string
-        url_suffix[url_suffix.length - 1] = suffices.substring(0, p);
-      }
-      else
+      if (prop.equals("returns"))
       {
-        lastsep = (separators[pass - 1] = separators[pass - 1].substring(0,
-                p));
+        _configureOurputFormatFrom(val, warnings);
       }
-      suffices = separators[pass];
-      pass++;
     }
-    if (pass > 0)
+  }
+
+  private String _genOutputFormatString()
+  {
+    String buff = "";
+    if (resultData==null)
     {
-      lastsep = separators[pass - 1];
+      return "";
     }
-    // last separator is always used for all the remaining separators
-    while (pass < separators.length)
+    for (JvDataType type : resultData)
     {
-      separators[pass++] = lastsep;
+      if (buff.length() > 0)
+      {
+        buff += ";";
+      }
+      buff += type.toString();
     }
+    return buff;
   }
 
-  /**
-   * @return the url_suffix
-   */
-  public String getUrl_suffix()
-  {
-    return url_suffix[url_suffix.length - 1];
-  }
-
-  /**
-   * @return the url_prefix
-   */
-  public String getUrl_prefix()
-  {
-    return url_prefix;
-  }
-
-  /**
-   * @return the target
-   */
-  public String getTarget()
-  {
-    return target;
-  }
-
-  /**
-   * @return the label
-   */
-  public String getLabel()
-  {
-    return label;
-  }
-
-  /**
-   * @return the sequence ID regexReplace
-   */
-  public String getIDRegexReplace()
+  private void _configureOurputFormatFrom(String outstring,
+          StringBuffer warnings)
   {
-    return _replaceFor(tokens[0]);
-  }
-
-  private String _replaceFor(String token)
-  {
-    for (int i = 0; i < mtch.length; i++)
-      if (segs[i] > -1 && mtch[i].equals(token))
+    StringTokenizer st = new StringTokenizer(outstring, ";");
+    String tok = "";
+    resultData = new ArrayList<JvDataType>();
+    while (st.hasMoreTokens())
+    {
+      try
+      {
+        resultData.add(JvDataType.valueOf(tok = st.nextToken()));
+      } catch (NoSuchElementException x)
       {
-        return regexReplace[i];
+        warnings.append("Invalid result type: '" + tok
+                + "' (must be one of: ");
+        String sep = "";
+        for (JvDataType vl : JvDataType.values())
+        {
+          warnings.append(sep);
+          warnings.append(vl.toString());
+          sep = " ,";
+        }
+        warnings.append(" separated by semi-colons)\n");
       }
-    return null;
+    }
   }
 
-  /**
-   * @return the sequence ID regexReplace
-   */
-  public String getSeqRegexReplace()
+  private String getServiceIOProperties()
   {
-    return _replaceFor(tokens[1]);
-  }
+    String[] vls = new String[]
+    { isHseparable() ? "hseparable" : "",
+        isVseparable() ? "vseparable" : "",
+        (new String("gapCharacter='" + gapCharacter + "'")),
+        (new String("returns='" + _genOutputFormatString() + "'")) };
 
-  /**
-   * @return the invalidMessage
-   */
-  public String getInvalidMessage()
-  {
-    return invalidMessage;
+    return arrayToSeparatorList(vls, ",");
   }
 
-  /**
-   * Check if URL string was parsed properly.
-   * 
-   * @return boolean - if false then <code>getInvalidMessage</code> returns an
-   *         error message
-   */
-  public boolean isValid()
+  public String toString()
   {
-    return invalidMessage == null;
+    StringBuffer result = new StringBuffer();
+    result.append(details.Name);
+    result.append('|');
+    result.append(details.Action);
+    result.append('|');
+    if (details.description != null)
+    {
+      result.append(details.description);
+    }
+    ;
+    // list job input flags
+    result.append('|');
+    result.append(getServiceIOProperties());
+    // list any additional cgi parameters needed for result retrieval
+    if (urlSuffix != null && urlSuffix.length() > 0)
+    {
+      result.append('|');
+      result.append(urlSuffix);
+    }
+    result.append('|');
+    result.append(getInputParamEncodedUrl());
+    return result.toString();
   }
 
-
-  /**
-   * gathers input into a hashtable
-   * 
-   * @param idstrings
-   * @param seqstrings
-   * @param dsstring
-   * @return
-   */
-  private Hashtable replacementArgs(String[] idstrings,
-          String[] seqstrings, String dsstring)
-  {
-    Hashtable rstrings = new Hashtable();
-    rstrings.put(tokens[0], idstrings);
-    rstrings.put(tokens[1], seqstrings);
-    rstrings.put(tokens[2], new String[]
-    { dsstring });
-    if (idstrings.length != seqstrings.length)
-    {
-      throw new Error(
-              "idstrings and seqstrings contain one string each per sequence.");
+  public boolean configureFromEncodedString(String encoding,
+          StringBuffer warnings)
+  {
+    boolean valid = false;
+    String[] list = separatorListToArray(encoding, "|");
+    details.Name = list[0];
+    details.Action = list[1];
+    details.description = list[2];
+    configureFromServiceInputProperties(list[3], warnings);
+    if (list.length > 5)
+    {
+      urlSuffix = list[4];
+      valid |= configureFromInputParamEncodedUrl(list[5], warnings);
     }
-    return rstrings;
+    else
+    {
+      urlSuffix = null;
+      valid |= configureFromInputParamEncodedUrl(list[4], warnings);
+    }
+    return valid;
   }
 
-
-
-
   /**
-   * conditionally generate urls or stubs for a given input.
-   * 
-   * @param createFullUrl
-   *          set to false if you only want to test if URLs would be generated.
-   * @param repstrings
-   * @param onlyIfMatches
-   * @return null if no url is generated. Object[] { int[] { number of matches
-   *         seqs }, boolean[] { which matched }, (if createFullUrl also has
-   *         StringBuffer[] { segment generated from inputs that is used in URL
-   *         }, String[] { url })}
-   * @throws Exception 
-   * @throws UrlStringTooLongException
+   * @return string representation of the input parameters, their type and
+   *         constraints, appended to the service's base submission URL
    */
-  protected Object[] makeUrlsIf(boolean createFullUrl,
-          Hashtable repstrings, boolean onlyIfMatches) throws Exception
+  private String getInputParamEncodedUrl()
   {
-    int pass = 0;
+    StringBuffer url = new StringBuffer();
+    if (postUrl == null || postUrl.length() < 5)
+    {
+      return "";
+    }
 
-    // prepare string arrays in correct order to be assembled into URL input
-    String[][] idseq = new String[mtch.length][]; // indexed by pass
-    int mins = 0, maxs = 0; // allowed two values, 1 or n-sequences.
-    for (int i = 0; i < mtch.length; i++)
+    url.append(postUrl);
+    char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
+    boolean consts = true;
+    do
     {
-      idseq[i] = (String[]) repstrings.get(mtch[i]);
-      if (idseq[i].length >= 1)
+      for (Map.Entry<String, InputType> param : inputParams.entrySet())
       {
-        if (mins == 0 && idseq[i].length == 1)
-        {
-          mins = 1;
-        }
-        if (maxs < 2)
+        List<String> vals = param.getValue().getURLEncodedParameter();
+        if (param.getValue().isConstant())
         {
-          maxs = idseq[i].length;
+          if (consts)
+          {
+            url.append(appendChar);
+            appendChar = '&';
+            url.append(param.getValue().token);
+            if (vals.size() == 1)
+            {
+              url.append("=");
+              url.append(vals.get(0));
+            }
+          }
         }
         else
         {
-          if (maxs != idseq[i].length)
+          if (!consts)
           {
-            throw new Error(
-                    "Cannot have mixed length replacement vectors. Replacement vector for "
-                            + (mtch[i]) + " is " + idseq[i].length
-                            + " strings long, and have already seen a "
-                            + maxs + " length vector.");
+            url.append(appendChar);
+            appendChar = '&';
+            url.append(param.getValue().token);
+            url.append("=");
+            // write parameter set as $TOKENPREFIX:csv list of params$ for this
+            // input param
+            url.append("$");
+            url.append(param.getValue().getURLtokenPrefix());
+            url.append(":");
+            url.append(arrayToSeparatorList(vals.toArray(new String[0]),
+                    ","));
+            url.append("$");
           }
         }
+
       }
-      else
-      {
-        throw new Error(
-                "Cannot have zero length vector of replacement strings - either 1 value or n values.");
-      }
-    }
-    // iterate through input, collating segments to be inserted into url
-    StringBuffer matched[] = new StringBuffer[idseq.length];
-    // and precompile regexes
-    com.stevesoft.pat.Regex[] rgxs = new com.stevesoft.pat.Regex[matched.length];
-    for (pass = 0; pass < matched.length; pass++)
-    {
-      matched[pass] = new StringBuffer();
-      if (regexReplace[pass] != null)
+      // toggle consts and repeat until !consts is false:
+    } while (!(consts = !consts));
+    return url.toString();
+  }
+
+  /**
+   * parse the service URL and input parameters from the given encoded URL
+   * string and configure the RestServiceDescription from it.
+   * 
+   * @param ipurl
+   * @param warnings
+   *          where any warnings
+   * @return true if URL parsed correctly. false means the configuration failed.
+   */
+  private boolean configureFromInputParamEncodedUrl(String ipurl,
+          StringBuffer warnings)
+  {
+    boolean valid = true;
+    int lastp = 0;
+    String url = new String();
+    Matcher prms = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$")
+            .matcher(ipurl);
+    Map<String, InputType> iparams = new Hashtable<String, InputType>();
+    InputType jinput;
+    while (prms.find())
+    {
+      if (lastp < prms.start(0))
       {
-        rgxs[pass] = com.stevesoft.pat.Regex.perlCode("/"
-                + regexReplace[pass] + "/");
+        url += ipurl.substring(lastp, prms.start(0));
+        lastp = prms.end(0) + 1;
       }
-      else
+      String sep = prms.group(1);
+      String tok = prms.group(2);
+      String iprm = prms.group(3);
+      int colon = iprm.indexOf(":");
+      String iprmparams = "";
+      if (colon > -1)
       {
-        rgxs[pass] = null;
+        iprmparams = iprm.substring(colon + 1);
+        iprm = iprm.substring(0, colon);
       }
-    }
-    // tot up the invariant lengths for this url
-    int urllength = url_prefix.length();
-    for (pass = 0; pass < matched.length; pass++)
-    {
-      urllength += url_suffix[pass].length();
-    }
-
-    // flags to record which of the input sequences were actually used to
-    // generate the
-    // url
-    boolean[] thismatched = new boolean[maxs];
-    int seqsmatched = 0;
-    for (int sq = 0; sq < maxs; sq++)
-    {
-      // initialise flag for match
-      thismatched[sq] = false;
-      StringBuffer[] thematches = new StringBuffer[rgxs.length];
-      for (pass = 0; pass < rgxs.length; pass++)
+      // TODO - find a better way of maintaining this classlist
+      for (Class type : new Class[]
+      { jalview.ws.rest.params.Alignment.class,
+          jalview.ws.rest.params.AnnotationFile.class,
+          SeqGroupIndexVector.class,
+          jalview.ws.rest.params.SeqIdVector.class,
+          jalview.ws.rest.params.SeqVector.class,
+          jalview.ws.rest.params.Tree.class })
       {
-        thematches[pass] = new StringBuffer(); // initialise - in case there are
-                                               // no more
-        // matches.
-        // if a regex is provided, then it must match for all sequences in all
-        // tokens for it to be considered.
-        if (idseq[pass].length <= sq)
-        {
-          // no more replacement strings to try for this token
-          continue;
-        }
-        if (rgxs[pass] != null)
+        try
         {
-          com.stevesoft.pat.Regex rg = rgxs[pass];
-          int rematchat = 0;
-          // concatenate all matches of re in the given string!
-          while (rg.searchFrom(idseq[pass][sq], rematchat))
+          jinput = (InputType) (type.getConstructor().newInstance(null));
+          if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
           {
-            rematchat = rg.matchedTo();
-            thismatched[sq] |= true;
-            urllength += rg.charsMatched(); // count length
-            if ((urllength + 32) > Platform.getMaxCommandLineLength())
+            ArrayList<String> al = new ArrayList<String>();
+            for (String prprm : separatorListToArray(iprmparams, ","))
             {
-              throw new Exception("urllength");
+              al.add(prprm);
             }
-
-            if (!createFullUrl)
-            {
-              continue; // don't bother making the URL replacement text.
-            }
-            // do we take the cartesian products of the substituents ?
-            int ns = rg.numSubs();
-            if (ns == 0)
+            if (!jinput.configureFromURLtokenString(al, warnings))
             {
-              thematches[pass].append(rg.stringMatched());// take whole regex
+              valid = false;
+              warnings.append("Failed to parse '" + prms.group(0)
+                      + "' as a " + jinput.getURLtokenPrefix()
+                      + " input tag.\n");
             }
-            /*
-             * else if (ns==1) { // take only subgroup match return new String[]
-             * { rg.stringMatched(1), url_prefix+rg.stringMatched(1)+url_suffix
-             * }; }
-             */
-            // deal with multiple submatch case - for moment we do the simplest
-            // - concatenate the matched regions, instead of creating a complete
-            // list for each alternate match over all sequences.
-            // TODO: specify a 'replace pattern' - next refinement
             else
             {
-              // debug
-              /*
-               * for (int s = 0; s <= rg.numSubs(); s++) {
-               * System.err.println("Sub " + s + " : " + rg.matchedFrom(s) +
-               * " : " + rg.matchedTo(s) + " : '" + rg.stringMatched(s) + "'");
-               * }
-               */
-              // try to collate subgroup matches
-              StringBuffer subs = new StringBuffer();
-              // have to loop through submatches, collating them at top level
-              // match
-              int s = 0; // 1;
-              while (s <= ns)
-              {
-                if (s + 1 <= ns && rg.matchedTo(s) > -1
-                        && rg.matchedTo(s + 1) > -1
-                        && rg.matchedTo(s + 1) < rg.matchedTo(s))
-                {
-                  // s is top level submatch. search for submatches enclosed by
-                  // this one
-                  int r = s + 1;
-                  StringBuffer rmtch = new StringBuffer();
-                  while (r <= ns && rg.matchedTo(r) <= rg.matchedTo(s))
-                  {
-                    if (rg.matchedFrom(r) > -1)
-                    {
-                      rmtch.append(rg.stringMatched(r));
-                    }
-                    r++;
-                  }
-                  if (rmtch.length() > 0)
-                  {
-                    subs.append(rmtch); // simply concatenate
-                  }
-                  s = r;
-                }
-                else
-                {
-                  if (rg.matchedFrom(s) > -1)
-                  {
-                    subs.append(rg.stringMatched(s)); // concatenate
-                  }
-                  s++;
-                }
-              }
-              thematches[pass].append(subs);
-            }
-          }
-        }
-        else
-        {
-          // are we only supposed to take regex matches ?
-          if (!onlyIfMatches)
-          {
-            thismatched[sq] |= true;
-            urllength += idseq[pass][sq].length(); // tot up length
-            if (createFullUrl)
-            {
-              thematches[pass] = new StringBuffer(idseq[pass][sq]); // take
-                                                                    // whole
-                                                                    // string -
-              // regardless - probably not a
-              // good idea!
-              /*
-               * TODO: do some boilerplate trimming of the fields to make them
-               * sensible e.g. trim off any 'prefix' in the id string (see
-               * UrlLink for the below) - pre 2.4 Jalview behaviour if
-               * (idstring.indexOf("|") > -1) { idstring =
-               * idstring.substring(idstring.lastIndexOf("|") + 1); }
-               */
+              jinput.token = tok;
+              iparams.put(tok, jinput);
             }
-
+            break;
           }
-        }
-      }
 
-      // check if we are going to add this sequence's results ? all token
-      // replacements must be valid for this to happen!
-      // (including single value replacements - eg. dataset name)
-      if (thismatched[sq])
-      {
-        if (createFullUrl)
+        } catch (Throwable thr)
         {
-          for (pass = 0; pass < matched.length; pass++)
-          {
-            if (idseq[pass].length > 1 && matched[pass].length() > 0)
-            {
-              matched[pass].append(separators[pass]);
-            }
-            matched[pass].append(thematches[pass]);
-          }
         }
-        seqsmatched++;
+        ;
       }
     }
-    // finally, if any sequences matched, then form the URL and return
-    if (seqsmatched == 0 || (createFullUrl && matched[0].length() == 0))
-    {
-      // no matches - no url generated
-      return null;
-    }
-    // check if we are beyond the feasible command line string limit for this
-    // platform
-    if ((urllength + 32) > Platform.getMaxCommandLineLength())
-    {
-      throw new Exception("urllength");
-    }
-    if (!createFullUrl)
-    {
-      // just return the essential info about what the URL would be generated
-      // from
-      return new Object[]
-      { new int[]
-      { seqsmatched }, thismatched };
-    }
-    // otherwise, create the URL completely.
-
-    StringBuffer submiturl = new StringBuffer();
-    submiturl.append(url_prefix);
-    for (pass = 0; pass < matched.length; pass++)
+    if (valid)
     {
-      submiturl.append(matched[pass]);
-      if (url_suffix[pass] != null)
+      try
       {
-        submiturl.append(url_suffix[pass]);
+        URL u = new URL(url);
+        postUrl = url;
+        inputParams = iparams;
+      } catch (Exception e)
+      {
+        warnings.append("Failed to parse '" + url + "' as a URL.\n");
+        valid = false;
       }
     }
-
-    return new Object[]
-    { new int[]
-    { seqsmatched }, thismatched, matched, new String[]
-    { submiturl.toString() } };
-  }
-
-  /**
-   * 
-   * @param urlstub
-   * @return number of distinct sequence (id or seuqence) replacements predicted
-   *         for this stub
-   */
-  public int getNumberInvolved(Object[] urlstub)
-  {
-    return ((int[]) urlstub[0])[0]; // returns seqsmatched from
-                                    // makeUrlsIf(false,...)
+    return valid;
   }
 
-  /**
-   * get token types present in this url as a bitfield indicating presence of
-   * each token from tokens (LSB->MSB).
-   * 
-   * @return groupURL class as integer
-   */
-  public int getGroupURLType()
+  public static void main(String argv[])
   {
-    int r = 0;
-    for (int pass = 0; pass < tokens.length; pass++)
+    if (argv.length == 0)
     {
-      for (int i = 0; i < mtch.length; i++)
+      if (!testRsdExchange("Test using default Shmmr service",
+              RestClient.makeShmmrRestClient().service))
       {
-        if (mtch[i].equals(tokens[pass]))
-        {
-          r += 1 << pass;
-        }
+        System.err.println("default test failed.");
+      }
+      else
+      {
+        System.err.println("default test passed.");
       }
     }
-    return r;
+    else
+    {
+      int i = 0, p = 0;
+      for (String svc : argv)
+      {
+        p += testRsdExchange("Test " + (++i), svc) ? 1 : 0;
+      }
+      System.err.println("" + p + " out of " + i + " tests passed.");
+
+    }
   }
 
-  public String toString()
+  private static boolean testRsdExchange(String desc, String servicestring)
   {
-    StringBuffer result = new StringBuffer();
-    result.append(label + "|" + url_prefix);
-    int r;
-    for (r = 0; r < url_suffix.length; r++)
+    try
     {
-      result.append("$");
-      result.append(mtch[r]);
-      if (regexReplace[r] != null)
+      RestServiceDescription newService = new RestServiceDescription(
+              servicestring);
+      if (!newService.isValid())
       {
-        result.append("=/");
-        result.append(regexReplace[r]);
-        result.append("/=");
+        throw new Error("Failed to create service from '" + servicestring
+                + "'.\n" + newService.getInvalidMessage());
       }
-      result.append("$");
-      result.append(url_suffix[r]);
-    }
-    for (r = 0; r < separators.length; r++)
+      return testRsdExchange(desc, newService);
+    } catch (Throwable x)
     {
-      result.append("|");
-      result.append(separators[r]);
+      System.err.println("Failed for service (" + desc + "): "
+              + servicestring);
+      x.printStackTrace();
+      return false;
     }
-    return result.toString();
   }
 
-  /**
-   * report stats about the generated url string given an input set
-   * 
-   * @param ul
-   * @param idstring
-   * @param url
-   */
-  private static void testUrls(RestServiceDescription
-          ul, String[][] idstring,
-          Object[] url)
+  private static boolean testRsdExchange(String desc,
+          RestServiceDescription service)
   {
-
-    if (url == null)
+    try
     {
-      System.out.println("Created NO urls.");
-    }
-    else
-    {
-      System.out.println("Created a url from " + ((int[]) url[0])[0]
-              + "out of " + idstring[0].length + " sequences.");
-      System.out.println("Sequences that did not match:");
-      for (int sq = 0; sq < idstring[0].length; sq++)
+      String fromservicetostring = service.toString();
+      RestServiceDescription newService = new RestServiceDescription(
+              fromservicetostring);
+      if (!newService.isValid())
       {
-        if (!((boolean[]) url[1])[sq])
-        {
-          System.out.println("Seq " + sq + ": " + idstring[0][sq] + "\t: "
-                  + idstring[1][sq]);
-        }
+        throw new Error("Failed to create service from '"
+                + fromservicetostring + "'.\n"
+                + newService.getInvalidMessage());
       }
-      System.out.println("Sequences that DID match:");
-      for (int sq = 0; sq < idstring[0].length; sq++)
+
+      if (!service.equals(newService))
       {
-        if (((boolean[]) url[1])[sq])
-        {
-          System.out.println("Seq " + sq + ": " + idstring[0][sq] + "\t: "
-                  + idstring[1][sq]);
-        }
+        System.err.println("Failed for service (" + desc + ").");
+        System.err.println("Original service and parsed service differ.");
+        System.err.println("Original: " + fromservicetostring);
+        System.err.println("Parsed  : " + newService.toString());
+        return false;
       }
-      System.out.println("The generated URL:");
-      System.out.println(((String[]) url[3])[0]);
+    } catch (Throwable x)
+    {
+      System.err.println("Failed for service (" + desc + "): "
+              + service.toString());
+      x.printStackTrace();
+      return false;
     }
-  }
-
-  public static void main(String argv[])
-  {
+    return true;
   }
 
   /**
@@ -865,41 +767,38 @@ public class RestServiceDescription
     return idset;
   }
 
-  public void setLabel(String newlabel)
-  {
-    this.label = newlabel;
-  }
-
   /**
-   * can this service be run on the visible portion of an alignment regardless of hidden boundaries ?
+   * can this service be run on the visible portion of an alignment regardless
+   * of hidden boundaries ?
    */
-  boolean hseparable=false;
-  boolean vseparable=false;
-  
+  boolean hseparable = false;
+
+  boolean vseparable = false;
+
   public boolean isHseparable()
   {
-    // TODO Auto-generated method stub
     return hseparable;
   }
+
   /**
    * 
    * @return
    */
   public boolean isVseparable()
   {
-    // TODO Auto-generated method stub
-    return hseparable;
+    return vseparable;
   }
 
   /**
    * search the input types for an instance of the given class
+   * 
    * @param <validInput.inputType> class1
    * @return
    */
   public boolean inputInvolves(Class<?> class1)
   {
-    assert(InputType.class.isAssignableFrom(class1));
-    for (InputType val:inputParams.values())
+    assert (InputType.class.isAssignableFrom(class1));
+    for (InputType val : inputParams.values())
     {
       if (class1.isAssignableFrom(val.getClass()))
       {
@@ -908,10 +807,13 @@ public class RestServiceDescription
     }
     return false;
   }
+
   char gapCharacter = '-';
+
   /**
    * 
-   * @return the preferred gap character for alignments input/output by this service 
+   * @return the preferred gap character for alignments input/output by this
+   *         service
    */
   public char getGapCharacter()
   {
@@ -921,31 +823,37 @@ public class RestServiceDescription
   public String getDecoratedResultUrl(String jobId)
   {
     // TODO: correctly write ?/& appropriate to result URL format.
-    return jobId+urlSuffix;
+    return jobId + urlSuffix;
   }
+
   private List<JvDataType> resultData;
+
   /**
    * 
    * 
-   * TODO: Extend to optionally specify relative/absolute url where data of this type can be retrieved from
+   * TODO: Extend to optionally specify relative/absolute url where data of this
+   * type can be retrieved from
+   * 
    * @param dt
    */
   public void addResultDatatype(JvDataType dt)
   {
-    if (resultData==null)
+    if (resultData == null)
     {
       resultData = new ArrayList<JvDataType>();
     }
     resultData.add(dt);
   }
+
   public boolean removeRsultDatatype(JvDataType dt)
   {
-    if (resultData!=null)
+    if (resultData != null)
     {
       return resultData.remove(dt);
     }
     return false;
   }
+
   public List<JvDataType> getResultDataTypes()
   {
     return resultData;