JAL-2089 patch broken merge to master for Release 2.10.0b1
[jalview.git] / src / jalview / ws / rest / RestServiceDescription.java
index dea7fc9..34a1224 100644 (file)
@@ -1,31 +1,30 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.6)
- * Copyright (C) 2010 J Procter, AM Waterhouse, G Barton, M Clamp, S Searle
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
  * Jalview is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
- * 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
  * Jalview is distributed in the hope that it will be useful, but 
  * WITHOUT ANY WARRANTY; without even the implied warranty 
  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
  * PURPOSE.  See the GNU General Public License for more details.
  * 
- * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
 package jalview.ws.rest;
 
 import jalview.datamodel.SequenceI;
-import jalview.io.packed.DataProvider;
-import jalview.io.packed.SimpleDataProvider;
 import jalview.io.packed.DataProvider.JvDataType;
-import jalview.util.GroupUrlLink.UrlStringTooLongException;
-import jalview.util.Platform;
+import jalview.util.StringUtils;
 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;
@@ -36,18 +35,22 @@ 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
 {
+  private static final Pattern PARAM_ENCODED_URL_PATTERN = Pattern
+          .compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$");
+
+  /**
+   * create a new rest service description ready to be configured
+   */
+  public RestServiceDescription()
+  {
+
+  }
+
   /**
    * @param details
    * @param postUrl
@@ -64,17 +67,21 @@ public class RestServiceDescription
   {
     super();
     this.details = new UIinfo();
-    details.Action = action;
-    details.description = description;
-    details.Name = name;
-    this.postUrl = postUrl;
-    this.urlSuffix = urlSuffix;
-    this.inputParams = inputParams;
+    details.Action = action == null ? "" : action;
+    details.description = description == null ? "" : description;
+    details.Name = name == null ? "" : name;
+    this.postUrl = postUrl == null ? "" : postUrl;
+    this.urlSuffix = urlSuffix == null ? "" : urlSuffix;
+    if (inputParams != null)
+    {
+      this.inputParams = inputParams;
+    }
     this.hseparable = hseparable;
     this.vseparable = vseparable;
     this.gapCharacter = gapCharacter;
   }
 
+  @Override
   public boolean equals(Object o)
   {
     if (o == null || !(o instanceof RestServiceDescription))
@@ -85,7 +92,8 @@ public class RestServiceDescription
     boolean diff = (gapCharacter != other.gapCharacter);
     diff |= vseparable != other.vseparable;
     diff |= hseparable != other.hseparable;
-    diff |= !(urlSuffix.equals(other.urlSuffix));
+    diff |= !(urlSuffix == null && other.urlSuffix == null || (urlSuffix != null
+            && other.urlSuffix != null && urlSuffix.equals(other.urlSuffix)));
     // TODO - robust diff that includes constants and reordering of URL
     // diff |= !(postUrl.equals(other.postUrl));
     // diff |= !inputParams.equals(other.inputParams);
@@ -101,6 +109,36 @@ public class RestServiceDescription
 
   public class UIinfo
   {
+    public String getAction()
+    {
+      return Action;
+    }
+
+    public void setAction(String action)
+    {
+      Action = action;
+    }
+
+    public String getName()
+    {
+      return Name;
+    }
+
+    public void setName(String name)
+    {
+      Name = name;
+    }
+
+    public String getDescription()
+    {
+      return description;
+    }
+
+    public void setDescription(String description)
+    {
+      this.description = description;
+    }
+
     String Action;
 
     String Name;
@@ -110,11 +148,86 @@ public class RestServiceDescription
 
   public UIinfo details = new UIinfo();
 
+  public String getAction()
+  {
+    return details.getAction();
+  }
+
+  public void setAction(String action)
+  {
+    details.setAction(action);
+  }
+
+  public String getName()
+  {
+    return details.getName();
+  }
+
+  public void setName(String name)
+  {
+    details.setName(name);
+  }
+
+  public String getDescription()
+  {
+    return details.getDescription();
+  }
+
+  public void setDescription(String description)
+  {
+    details.setDescription(description);
+  }
+
   /**
    * Service base URL
    */
   String postUrl;
 
+  public String getPostUrl()
+  {
+    return postUrl;
+  }
+
+  public void setPostUrl(String postUrl)
+  {
+    this.postUrl = postUrl;
+  }
+
+  public String getUrlSuffix()
+  {
+    return urlSuffix;
+  }
+
+  public void setUrlSuffix(String urlSuffix)
+  {
+    this.urlSuffix = urlSuffix;
+  }
+
+  public Map<String, InputType> getInputParams()
+  {
+    return inputParams;
+  }
+
+  public void setInputParams(Map<String, InputType> inputParams)
+  {
+    this.inputParams = inputParams;
+  }
+
+  public void setHseparable(boolean hseparable)
+  {
+    this.hseparable = hseparable;
+  }
+
+  public void setVseparable(boolean vseparable)
+  {
+    this.vseparable = vseparable;
+  }
+
+  public void setGapCharacter(char gapCharacter)
+  {
+    this.gapCharacter = gapCharacter;
+  }
+
   /**
    * suffix that should be added to any url used if it does not already end in
    * the suffix.
@@ -124,7 +237,7 @@ public class RestServiceDescription
   /**
    * input info given as key/value pairs - mapped to post arguments
    */
-  Map<String, InputType> inputParams = new HashMap();
+  Map<String, InputType> inputParams = new HashMap<String, InputType>();
 
   /**
    * assigns the given inputType it to its corresponding input parameter token
@@ -213,11 +326,21 @@ public class RestServiceDescription
 
   public RestServiceDescription(RestServiceDescription toedit)
   {
-    if (toedit == null)
-    {
-      return;
-    }
-    // TODO Implement copy constructor NOW
+    // Rather then do the above, we cheat and use our human readable
+    // serialization code to clone everything
+    this(toedit.toString());
+    /**
+     * if (toedit == null) { return; } /** urlSuffix = toedit.urlSuffix; postUrl
+     * = toedit.postUrl; hseparable = toedit.hseparable; vseparable =
+     * toedit.vseparable; gapCharacter = toedit.gapCharacter; details = new
+     * RestServiceDescription.UIinfo(); details.Action = toedit.details.Action;
+     * details.description = toedit.details.description; details.Name =
+     * toedit.details.Name; for (InputType itype: toedit.inputParams.values()) {
+     * inputParams.put(itype.token, itype.clone());
+     * 
+     * }
+     */
+    // TODO Implement copy constructor NOW*/
   }
 
   /**
@@ -239,121 +362,6 @@ public class RestServiceDescription
     return invalidMessage == null;
   }
 
-  private static boolean debug = false;
-
-  /**
-   * parse the string into a list
-   * 
-   * @param list
-   * @param separator
-   * @return elements separated by separator
-   */
-  public static String[] separatorListToArray(String list, String separator)
-  {
-    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)
-    {
-      escape = (list.charAt(pos - 1) == '\\') ? -1 : 0;
-      if (wasescaped)
-      {
-        // 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;
-    }
-    if (cp < list.length())
-    {
-      String c = list.substring(cp);
-      if (wasescaped)
-      {
-        // append final separator
-        jv.set(jv.size() - 1, lstitem + separator + c);
-      }
-      else
-      {
-        if (!c.equals(separator))
-        {
-          jv.add(c);
-        }
-      }
-    }
-    if (jv.size() > 0)
-    {
-      String[] v = jv.toArray(new String[jv.size()]);
-      jv.clear();
-      if (debug)
-      {
-        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;
-    }
-    if (debug)
-    {
-      System.err.println("Empty Array from '" + separator
-              + "' separated List");
-    }
-    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)
-    {
-      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();
-    }
-    if (debug)
-    {
-      System.err.println("Returning empty '" + separator
-              + "' separated List\n");
-    }
-    return "" + separator;
-  }
-
   /**
    * parse a string containing a list of service properties and configure the
    * service description
@@ -362,16 +370,18 @@ public class RestServiceDescription
    *          param warnings a StringBuffer that any warnings about invalid
    *          content will be appended to.
    */
-  private void configureFromServiceInputProperties(String propList,
+  private boolean configureFromServiceInputProperties(String propList,
           StringBuffer warnings)
   {
-    String[] props = separatorListToArray(propList, ",");
+    String[] props = StringUtils.separatorListToArray(propList, ",");
     if (props == null)
     {
-      return;
+      return true;
     }
     ;
+    boolean valid = true;
     String val = null;
+    int l = warnings.length();
     int i;
     for (String prop : props)
     {
@@ -395,8 +405,9 @@ public class RestServiceDescription
       }
       if (prop.equals("gapCharacter"))
       {
-        if (val == null || val.length() > 1)
+        if (val == null || val.length() == 0 || val.length() > 1)
         {
+          valid = false;
           warnings.append((warnings.length() > 0 ? "\n" : "")
                   + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
                           + val + "'"));
@@ -408,15 +419,17 @@ public class RestServiceDescription
       }
       if (prop.equals("returns"))
       {
-        _configureOurputFormatFrom(val, warnings);
+        _configureOutputFormatFrom(val, warnings);
       }
     }
+    // return true if valid is true and warning buffer was not appended to.
+    return valid && (l == warnings.length());
   }
 
   private String _genOutputFormatString()
   {
     String buff = "";
-    if (resultData==null)
+    if (resultData == null)
     {
       return "";
     }
@@ -431,9 +444,14 @@ public class RestServiceDescription
     return buff;
   }
 
-  private void _configureOurputFormatFrom(String outstring,
+  private void _configureOutputFormatFrom(String outstring,
           StringBuffer warnings)
   {
+    if (outstring.indexOf(";") == -1)
+    {
+      // we add a token, for simplicity
+      outstring = outstring + ";";
+    }
     StringTokenizer st = new StringTokenizer(outstring, ";");
     String tok = "";
     resultData = new ArrayList<JvDataType>();
@@ -460,18 +478,27 @@ public class RestServiceDescription
 
   private String getServiceIOProperties()
   {
-    String[] vls = new String[]
-    { isHseparable() ? "hseparable" : "",
-        isVseparable() ? "vseparable" : "",
-        (new String("gapCharacter='" + gapCharacter + "'")),
-        (new String("returns='" + _genOutputFormatString() + "'")) };
-
-    return arrayToSeparatorList(vls, ",");
+    ArrayList<String> vls = new ArrayList<String>();
+    if (isHseparable())
+    {
+      vls.add("hseparable");
+    }
+    ;
+    if (isVseparable())
+    {
+      vls.add("vseparable");
+    }
+    ;
+    vls.add(new String("gapCharacter='" + gapCharacter + "'"));
+    vls.add(new String("returns='" + _genOutputFormatString() + "'"));
+    return StringUtils
+            .arrayToSeparatorList(vls.toArray(new String[0]), ",");
   }
 
   public String toString()
   {
     StringBuffer result = new StringBuffer();
+    result.append("|");
     result.append(details.Name);
     result.append('|');
     result.append(details.Action);
@@ -495,26 +522,71 @@ public class RestServiceDescription
     return result.toString();
   }
 
+  /**
+   * processes a service encoded as a string (as generated by
+   * RestServiceDescription.toString()) Note - this will only use the first
+   * service definition encountered in the string to configure the service.
+   * 
+   * @param encoding
+   * @param warnings
+   *          - where warning messages are reported.
+   * @return true if configuration was parsed successfully.
+   */
   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)
+    String[] list = StringUtils.separatorListToArray(encoding, "|");
+
+    int nextpos = parseServiceList(list, warnings, 0);
+    if (nextpos > 0)
     {
-      urlSuffix = list[4];
-      valid |= configureFromInputParamEncodedUrl(list[5], warnings);
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * processes the given list from position p, attempting to configure the
+   * service from it. Service lists are formed by concatenating individual
+   * stringified services. The first character of a stringified service is '|',
+   * enabling this, and the parser will ignore empty fields in a '|' separated
+   * list when they fall outside a service definition.
+   * 
+   * @param list
+   * @param warnings
+   * @param p
+   * @return
+   */
+  protected int parseServiceList(String[] list, StringBuffer warnings, int p)
+  {
+    boolean invalid = false;
+    // look for the first non-empty position - expect it to be service name
+    while (list[p] != null && list[p].trim().length() == 0)
+    {
+      p++;
+    }
+    details.Name = list[p];
+    details.Action = list[p + 1];
+    details.description = list[p + 2];
+    invalid |= !configureFromServiceInputProperties(list[p + 3], warnings);
+    if (list.length - p > 5 && list[p + 5] != null
+            && list[p + 5].trim().length() > 5)
+    {
+      urlSuffix = list[p + 4];
+      invalid |= !configureFromInputParamEncodedUrl(list[p + 5], warnings);
+      p += 6;
     }
     else
     {
-      urlSuffix = null;
-      valid |= configureFromInputParamEncodedUrl(list[4], warnings);
+      if (list.length - p > 4 && list[p + 4] != null
+              && list[p + 4].trim().length() > 5)
+      {
+        urlSuffix = null;
+        invalid |= !configureFromInputParamEncodedUrl(list[p + 4], warnings);
+        p += 5;
+      }
     }
-    return valid;
+    return invalid ? -1 : p;
   }
 
   /**
@@ -564,8 +636,8 @@ public class RestServiceDescription
             url.append("$");
             url.append(param.getValue().getURLtokenPrefix());
             url.append(":");
-            url.append(arrayToSeparatorList(vals.toArray(new String[0]),
-                    ","));
+            url.append(StringUtils.arrayToSeparatorList(
+                    vals.toArray(new String[0]), ","));
             url.append("$");
           }
         }
@@ -591,8 +663,7 @@ public class RestServiceDescription
     boolean valid = true;
     int lastp = 0;
     String url = new String();
-    Matcher prms = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$")
-            .matcher(ipurl);
+    Matcher prms = PARAM_ENCODED_URL_PATTERN.matcher(ipurl);
     Map<String, InputType> iparams = new Hashtable<String, InputType>();
     InputType jinput;
     while (prms.find())
@@ -612,45 +683,8 @@ public class RestServiceDescription
         iprmparams = iprm.substring(colon + 1);
         iprm = iprm.substring(0, colon);
       }
-      // 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 })
-      {
-        try
-        {
-          jinput = (InputType) (type.getConstructor().newInstance(null));
-          if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
-          {
-            ArrayList<String> al = new ArrayList<String>();
-            for (String prprm : separatorListToArray(iprmparams, ","))
-            {
-              al.add(prprm);
-            }
-            if (!jinput.configureFromURLtokenString(al, warnings))
-            {
-              valid = false;
-              warnings.append("Failed to parse '" + prms.group(0)
-                      + "' as a " + jinput.getURLtokenPrefix()
-                      + " input tag.\n");
-            }
-            else
-            {
-              jinput.token = tok;
-              iparams.put(tok, jinput);
-            }
-            break;
-          }
-
-        } catch (Throwable thr)
-        {
-        }
-        ;
-      }
+      valid = parseTypeString(prms.group(0), tok, iprm, iprmparams,
+              iparams, warnings);
     }
     if (valid)
     {
@@ -668,84 +702,59 @@ public class RestServiceDescription
     return valid;
   }
 
-  public static void main(String argv[])
+  public static Class[] getInputTypes()
   {
-    if (argv.length == 0)
-    {
-      if (!testRsdExchange("Test using default Shmmr service",
-              RestClient.makeShmmrRestClient().service))
-      {
-        System.err.println("default test failed.");
-      }
-      else
-      {
-        System.err.println("default test passed.");
-      }
-    }
-    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.");
-
-    }
-  }
-
-  private static boolean testRsdExchange(String desc, String servicestring)
-  {
-    try
-    {
-      RestServiceDescription newService = new RestServiceDescription(
-              servicestring);
-      if (!newService.isValid())
-      {
-        throw new Error("Failed to create service from '" + servicestring
-                + "'.\n" + newService.getInvalidMessage());
-      }
-      return testRsdExchange(desc, newService);
-    } catch (Throwable x)
-    {
-      System.err.println("Failed for service (" + desc + "): "
-              + servicestring);
-      x.printStackTrace();
-      return false;
-    }
+    // TODO - find a better way of maintaining this classlist
+    return 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 };
   }
 
-  private static boolean testRsdExchange(String desc,
-          RestServiceDescription service)
+  public static boolean parseTypeString(String fullstring, String tok,
+          String iprm, String iprmparams, Map<String, InputType> iparams,
+          StringBuffer warnings)
   {
-    try
+    boolean valid = true;
+    InputType jinput;
+    for (Class type : getInputTypes())
     {
-      String fromservicetostring = service.toString();
-      RestServiceDescription newService = new RestServiceDescription(
-              fromservicetostring);
-      if (!newService.isValid())
+      try
       {
-        throw new Error("Failed to create service from '"
-                + fromservicetostring + "'.\n"
-                + newService.getInvalidMessage());
-      }
+        jinput = (InputType) (type.getConstructor().newInstance());
+        if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
+        {
+          ArrayList<String> al = new ArrayList<String>();
+          for (String prprm : StringUtils.separatorListToArray(iprmparams,
+                  ","))
+          {
+            // hack to ensure that strings like "sep=','" containing unescaped
+            // commas as values are concatenated
+            al.add(prprm.trim());
+          }
+          if (!jinput.configureFromURLtokenString(al, warnings))
+          {
+            valid = false;
+            warnings.append("Failed to parse '" + fullstring + "' as a "
+                    + jinput.getURLtokenPrefix() + " input tag.\n");
+          }
+          else
+          {
+            jinput.token = tok;
+            iparams.put(tok, jinput);
+            valid = true;
+          }
+          break;
+        }
 
-      if (!service.equals(newService))
+      } catch (Throwable thr)
       {
-        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;
       }
-    } catch (Throwable x)
-    {
-      System.err.println("Failed for service (" + desc + "): "
-              + service.toString());
-      x.printStackTrace();
-      return false;
+      ;
     }
-    return true;
+    return valid;
   }
 
   /**
@@ -826,7 +835,7 @@ public class RestServiceDescription
     return jobId + urlSuffix;
   }
 
-  private List<JvDataType> resultData;
+  private List<JvDataType> resultData = new ArrayList<JvDataType>();
 
   /**
    * 
@@ -858,4 +867,41 @@ public class RestServiceDescription
   {
     return resultData;
   }
+
+  /**
+   * parse a concatenated list of rest service descriptions into an array
+   * 
+   * @param services
+   * @return zero or more services.
+   * @throws exceptions
+   *           if the services are improperly encoded.
+   */
+  public static List<RestServiceDescription> parseDescriptions(
+          String services) throws Exception
+  {
+    String[] list = StringUtils.separatorListToArray(services, "|");
+    List<RestServiceDescription> svcparsed = new ArrayList<RestServiceDescription>();
+    int p = 0, lastp = 0;
+    StringBuffer warnings = new StringBuffer();
+    do
+    {
+      RestServiceDescription rsd = new RestServiceDescription();
+      p = rsd.parseServiceList(list, warnings, lastp = p);
+      if (p > lastp && rsd.isValid())
+      {
+        svcparsed.add(rsd);
+      }
+      else
+      {
+        throw new Exception(
+                "Failed to parse user defined RSBS services from :"
+                        + services
+                        + "\nFirst error was encountered at token " + lastp
+                        + " starting " + list[lastp] + ":\n"
+                        + rsd.getInvalidMessage());
+      }
+    } while (p < lastp && p < list.length - 1);
+    return svcparsed;
+  }
+
 }