cfaa76f5751abe1843203f80c777d5ed621fa57c
[jalview.git] / src / jalview / ws / rest / RestServiceDescription.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
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.
11  *  
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.
16  * 
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.
20  */
21 package jalview.ws.rest;
22
23 import jalview.datamodel.SequenceI;
24 import jalview.io.packed.DataProvider.JvDataType;
25 import jalview.util.StringUtils;
26 import jalview.ws.rest.params.Alignment;
27 import jalview.ws.rest.params.AnnotationFile;
28 import jalview.ws.rest.params.SeqGroupIndexVector;
29
30 import java.net.URL;
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.Hashtable;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.NoSuchElementException;
37 import java.util.StringTokenizer;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
40
41 public class RestServiceDescription
42 {
43   private static final Pattern PARAM_ENCODED_URL_PATTERN = Pattern.compile("([?&])([A-Za-z0-9_]+)=\\$([^$]+)\\$");
44
45   /**
46    * create a new rest service description ready to be configured
47    */
48   public RestServiceDescription()
49   {
50
51   }
52
53   /**
54    * @param postUrl
55    * @param urlSuffix
56    * @param urlPrefix
57    *          null or prefix for forming result URL from job
58    * @param inputParams
59    * @param hseparable
60    * @param vseparable
61    * @param gapCharacter
62    * @param details
63    */
64   public RestServiceDescription(String action, String description,
65           String name, String postUrl, String urlSuffix,
66           List<String> urlPrefix,
67           Map<String, InputType> inputParams,
68           boolean hseparable, boolean vseparable, char gapCharacter)
69   {
70     super();
71     this.details = new UIinfo();
72     details.Action = action == null ? "" : action;
73     details.description = description == null ? "" : description;
74     details.Name = name == null ? "" : name;
75     this.postUrl = postUrl == null ? "" : postUrl;
76     // TODO decide if nullable
77     this.rurlPref = urlPrefix;
78     this.urlSuffix = urlSuffix == null ? "" : urlSuffix;
79     if (inputParams != null)
80     {
81       this.inputParams = inputParams;
82     }
83     this.hseparable = hseparable;
84     this.vseparable = vseparable;
85     this.gapCharacter = gapCharacter;
86   }
87
88   @Override
89   public boolean equals(Object o)
90   {
91     if (o == null || !(o instanceof RestServiceDescription))
92     {
93       return false;
94     }
95     RestServiceDescription other = (RestServiceDescription) o;
96     boolean diff = (gapCharacter != other.gapCharacter);
97     diff |= vseparable != other.vseparable;
98     diff |= hseparable != other.hseparable;
99     diff |= !(urlSuffix == null && other.urlSuffix == null || (urlSuffix != null
100             && other.urlSuffix != null && urlSuffix.equals(other.urlSuffix)));
101     diff |= !(rurlPref == null && other.rurlPref == null || (rurlPref != null
102             && other.rurlPref != null && rurlPref.equals(other.rurlPref)));
103     // TODO - robust diff that includes constants and reordering of URL
104     // diff |= !(postUrl.equals(other.postUrl));
105     // diff |= !inputParams.equals(other.inputParams);
106     diff |= !details.Name.equals(other.details.Name);
107     diff |= !details.Action.equals(other.details.Action);
108     diff |= !details.description.equals(other.details.description);
109     return !diff;
110   }
111
112   /**
113    * Service UI Info { Action, Specific Name of Service, Brief Description }
114    */
115
116   public class UIinfo
117   {
118     public String getAction()
119     {
120       return Action;
121     }
122
123     public void setAction(String action)
124     {
125       Action = action;
126     }
127
128     public String getName()
129     {
130       return Name;
131     }
132
133     public void setName(String name)
134     {
135       Name = name;
136     }
137
138     public String getDescription()
139     {
140       return description;
141     }
142
143     public void setDescription(String description)
144     {
145       this.description = description;
146     }
147
148     String Action;
149
150     String Name;
151
152     String description;
153   }
154
155   public UIinfo details = new UIinfo();
156
157   public String getAction()
158   {
159     return details.getAction();
160   }
161
162   public void setAction(String action)
163   {
164     details.setAction(action);
165   }
166
167   public String getName()
168   {
169     return details.getName();
170   }
171
172   public void setName(String name)
173   {
174     details.setName(name);
175   }
176
177   public String getDescription()
178   {
179     return details.getDescription();
180   }
181
182   public void setDescription(String description)
183   {
184     details.setDescription(description);
185   }
186
187   /**
188    * Service base URL
189    */
190   String postUrl;
191
192   public String getPostUrl()
193   {
194     return postUrl;
195   }
196
197   public void setPostUrl(String postUrl)
198   {
199     this.postUrl = postUrl;
200   }
201
202   public String getUrlSuffix()
203   {
204     return urlSuffix;
205   }
206
207   public void setUrlSuffix(String urlSuffix)
208   {
209     this.urlSuffix = urlSuffix;
210   }
211
212   public Map<String, InputType> getInputParams()
213   {
214     return inputParams;
215   }
216
217   public void setInputParams(Map<String, InputType> inputParams)
218   {
219     this.inputParams = inputParams;
220   }
221
222   public void setHseparable(boolean hseparable)
223   {
224     this.hseparable = hseparable;
225   }
226
227   public void setVseparable(boolean vseparable)
228   {
229     this.vseparable = vseparable;
230   }
231
232   public void setGapCharacter(char gapCharacter)
233   {
234     this.gapCharacter = gapCharacter;
235   }
236
237   /**
238    * suffix that should be added to any url used if it does not already end in
239    * the suffix.
240    */
241   String urlSuffix;
242
243   /**
244    * prefix for poll and result URLs when null, urlSuffix is used
245    */
246   List<String> rurlPref = null;
247
248   /**
249    * input info given as key/value pairs - mapped to post arguments
250    */
251   Map<String, InputType> inputParams = new HashMap<String, InputType>();
252
253   /**
254    * assigns the given inputType it to its corresponding input parameter token
255    * it.token
256    * 
257    * @param it
258    */
259   public void setInputParam(InputType it)
260   {
261     inputParams.put(it.token, it);
262   }
263
264   /**
265    * remove the given input type it from the set of service input parameters.
266    * 
267    * @param it
268    */
269   public void removeInputParam(InputType it)
270   {
271     inputParams.remove(it.token);
272   }
273
274   /**
275    * service requests alignment data
276    */
277   boolean aligndata;
278
279   /**
280    * service requests alignment and/or seuqence annotationo data
281    */
282   boolean annotdata;
283
284   /**
285    * service requests partitions defined over input (alignment) data
286    */
287   boolean partitiondata;
288
289   /**
290    * process ths input data and set the appropriate shorthand flags describing
291    * the input the service wants
292    */
293   public void setInvolvesFlags()
294   {
295     aligndata = inputInvolves(Alignment.class);
296     annotdata = inputInvolves(AnnotationFile.class);
297     partitiondata = inputInvolves(SeqGroupIndexVector.class);
298   }
299
300   /**
301    * Service return info { alignment, annotation file (loaded back on to
302    * alignment), tree (loaded back on to alignment), sequence annotation -
303    * loaded back on to alignment), text report, pdb structures with sequence
304    * mapping )
305    * 
306    */
307
308   /**
309    * Start with bare minimum: input is alignment + groups on alignment
310    * 
311    * @author JimP
312    * 
313    */
314
315   private String invalidMessage = null;
316
317   /**
318    * parse the given linkString of the form '<label>|<url>|separator
319    * char[|optional sequence separator char]' into parts. url may contain a
320    * string $SEQUENCEIDS<=optional regex=>$ where <=optional regex=> must be of
321    * the form =/<perl style regex>/=$ or $SEQUENCES<=optional regex=>$ or
322    * $SEQUENCES<=optional regex=>$.
323    * 
324    * @param link
325    */
326   public RestServiceDescription(String link)
327   {
328     StringBuffer warnings = new StringBuffer();
329     if (!configureFromEncodedString(link, warnings))
330     {
331       if (warnings.length() > 0)
332       {
333         invalidMessage = warnings.toString();
334       }
335     }
336   }
337
338   public RestServiceDescription(RestServiceDescription toedit)
339   {
340     // Rather then do the above, we cheat and use our human readable
341     // serialization code to clone everything
342     this(toedit.toString());
343     /**
344      * if (toedit == null) { return; } /** urlSuffix = toedit.urlSuffix; postUrl
345      * = toedit.postUrl; hseparable = toedit.hseparable; vseparable =
346      * toedit.vseparable; gapCharacter = toedit.gapCharacter; details = new
347      * RestServiceDescription.UIinfo(); details.Action = toedit.details.Action;
348      * details.description = toedit.details.description; details.Name =
349      * toedit.details.Name; for (InputType itype: toedit.inputParams.values()) {
350      * inputParams.put(itype.token, itype.clone());
351      * 
352      * }
353      */
354     // TODO Implement copy constructor NOW*/
355   }
356
357   /**
358    * @return the invalidMessage
359    */
360   public String getInvalidMessage()
361   {
362     return invalidMessage;
363   }
364
365   /**
366    * Check if URL string was parsed properly.
367    * 
368    * @return boolean - if false then <code>getInvalidMessage</code> returns an
369    *         error message
370    */
371   public boolean isValid()
372   {
373     return invalidMessage == null;
374   }
375
376   /**
377    * parse a string containing a list of service properties and configure the
378    * service description
379    * 
380    * @param propList
381    *          param warnings a StringBuffer that any warnings about invalid
382    *          content will be appended to.
383    */
384   private boolean configureFromServiceInputProperties(String propList,
385           StringBuffer warnings)
386   {
387     String[] props = StringUtils.separatorListToArray(propList, ",");
388     if (props == null)
389     {
390       return true;
391     }
392     ;
393     boolean valid = true;
394     String val = null;
395     int l = warnings.length();
396     int i;
397     for (String prop : props)
398     {
399       if ((i = prop.indexOf("=")) > -1)
400       {
401         val = prop.substring(i + 1);
402         if (val.startsWith("\'") && val.endsWith("\'"))
403         {
404           val = val.substring(1, val.length() - 1);
405         }
406         prop = prop.substring(0, i);
407       }
408
409       if (prop.equals("hseparable"))
410       {
411         hseparable = true;
412       }
413       if (prop.equals("vseparable"))
414       {
415         vseparable = true;
416       }
417       if (prop.equals("gapCharacter"))
418       {
419         if (val == null || val.length() == 0 || val.length() > 1)
420         {
421           valid = false;
422           warnings.append((warnings.length() > 0 ? "\n" : "")
423                   + ("Invalid service property: gapCharacter=' ' (single character) - was given '"
424                           + val + "'"));
425         }
426         else
427         {
428           gapCharacter = val.charAt(0);
429         }
430       }
431       if (prop.equals("returns"))
432       {
433         _configureOutputFormatFrom(val, warnings);
434       }
435     }
436     // return true if valid is true and warning buffer was not appended to.
437     return valid && (l == warnings.length());
438   }
439
440   private String _genOutputFormatString()
441   {
442     String buff = "";
443     if (resultData == null)
444     {
445       return "";
446     }
447     for (JvDataType type : resultData)
448     {
449       if (buff.length() > 0)
450       {
451         buff += ";";
452       }
453       buff += type.toString();
454     }
455     return buff;
456   }
457
458   private void _configureOutputFormatFrom(String outstring,
459           StringBuffer warnings)
460   {
461     if (outstring.indexOf(";") == -1)
462     {
463       // we add a token, for simplicity
464       outstring = outstring + ";";
465     }
466     StringTokenizer st = new StringTokenizer(outstring, ";");
467     String tok = "";
468     resultData = new ArrayList<JvDataType>();
469     while (st.hasMoreTokens())
470     {
471       try
472       {
473         resultData.add(JvDataType.valueOf(tok = st.nextToken()));
474       } catch (NoSuchElementException x)
475       {
476         warnings.append("Invalid result type: '" + tok
477                 + "' (must be one of: ");
478         String sep = "";
479         for (JvDataType vl : JvDataType.values())
480         {
481           warnings.append(sep);
482           warnings.append(vl.toString());
483           sep = " ,";
484         }
485         warnings.append(" separated by semi-colons)\n");
486       }
487     }
488   }
489
490   private String getServiceIOProperties()
491   {
492     ArrayList<String> vls = new ArrayList<String>();
493     if (isHseparable())
494     {
495       vls.add("hseparable");
496     }
497     ;
498     if (isVseparable())
499     {
500       vls.add("vseparable");
501     }
502     ;
503     vls.add(new String("gapCharacter='" + gapCharacter + "'"));
504     vls.add(new String("returns='" + _genOutputFormatString() + "'"));
505     return StringUtils.arrayToSeparatorList(vls.toArray(new String[0]), ",");
506   }
507
508   public String toString()
509   {
510     StringBuffer result = new StringBuffer();
511     result.append("|");
512     result.append(details.Name);
513     result.append('|');
514     result.append(details.Action);
515     result.append('|');
516     if (details.description != null)
517     {
518       result.append(details.description);
519     }
520     ;
521     // list job input flags
522     result.append('|');
523     result.append(getServiceIOProperties());
524     // list any additional cgi parameters needed for result retrieval
525     if (urlSuffix != null && urlSuffix.length() > 0)
526     {
527       result.append('|');
528       result.append(urlSuffix);
529     }
530     result.append('|');
531     result.append(getInputParamEncodedUrl());
532     return result.toString();
533   }
534
535   /**
536    * processes a service encoded as a string (as generated by
537    * RestServiceDescription.toString()) Note - this will only use the first
538    * service definition encountered in the string to configure the service.
539    * 
540    * @param encoding
541    * @param warnings
542    *          - where warning messages are reported.
543    * @return true if configuration was parsed successfully.
544    */
545   public boolean configureFromEncodedString(String encoding,
546           StringBuffer warnings)
547   {
548     String[] list = StringUtils.separatorListToArray(encoding, "|");
549
550     int nextpos = parseServiceList(list, warnings, 0);
551     if (nextpos > 0)
552     {
553       return true;
554     }
555     return false;
556   }
557
558   /**
559    * processes the given list from position p, attempting to configure the
560    * service from it. Service lists are formed by concatenating individual
561    * stringified services. The first character of a stringified service is '|',
562    * enabling this, and the parser will ignore empty fields in a '|' separated
563    * list when they fall outside a service definition.
564    * 
565    * @param list
566    * @param warnings
567    * @param p
568    * @return
569    */
570   protected int parseServiceList(String[] list, StringBuffer warnings, int p)
571   {
572     boolean invalid = false;
573     // look for the first non-empty position - expect it to be service name
574     while (list[p] != null && list[p].trim().length() == 0)
575     {
576       p++;
577     }
578     details.Name = list[p];
579     details.Action = list[p + 1];
580     details.description = list[p + 2];
581     invalid |= !configureFromServiceInputProperties(list[p + 3], warnings);
582     if (list.length - p > 5 && list[p + 5] != null
583             && list[p + 5].trim().length() > 5)
584     {
585       urlSuffix = list[p + 4];
586       invalid |= !configureFromInputParamEncodedUrl(list[p + 5], warnings);
587       p += 6;
588     }
589     else
590     {
591       if (list.length - p > 4 && list[p + 4] != null
592               && list[p + 4].trim().length() > 5)
593       {
594         urlSuffix = null;
595         invalid |= !configureFromInputParamEncodedUrl(list[p + 4], warnings);
596         p += 5;
597       }
598     }
599     return invalid ? -1 : p;
600   }
601
602   /**
603    * @return string representation of the input parameters, their type and
604    *         constraints, appended to the service's base submission URL
605    */
606   private String getInputParamEncodedUrl()
607   {
608     StringBuffer url = new StringBuffer();
609     if (postUrl == null || postUrl.length() < 5)
610     {
611       return "";
612     }
613
614     url.append(postUrl);
615     char appendChar = (postUrl.indexOf("?") > -1) ? '&' : '?';
616     boolean consts = true;
617     do
618     {
619       for (Map.Entry<String, InputType> param : inputParams.entrySet())
620       {
621         List<String> vals = param.getValue().getURLEncodedParameter();
622         if (param.getValue().isConstant())
623         {
624           if (consts)
625           {
626             url.append(appendChar);
627             appendChar = '&';
628             url.append(param.getValue().token);
629             if (vals.size() == 1)
630             {
631               url.append("=");
632               url.append(vals.get(0));
633             }
634           }
635         }
636         else
637         {
638           if (!consts)
639           {
640             url.append(appendChar);
641             appendChar = '&';
642             url.append(param.getValue().token);
643             url.append("=");
644             // write parameter set as $TOKENPREFIX:csv list of params$ for this
645             // input param
646             url.append("$");
647             url.append(param.getValue().getURLtokenPrefix());
648             url.append(":");
649             url.append(StringUtils.arrayToSeparatorList(vals.toArray(new String[0]),
650                     ","));
651             url.append("$");
652           }
653         }
654
655       }
656       // toggle consts and repeat until !consts is false:
657     } while (!(consts = !consts));
658     return url.toString();
659   }
660
661   /**
662    * parse the service URL and input parameters from the given encoded URL
663    * string and configure the RestServiceDescription from it.
664    * 
665    * @param ipurl
666    * @param warnings
667    *          where any warnings
668    * @return true if URL parsed correctly. false means the configuration failed.
669    */
670   private boolean configureFromInputParamEncodedUrl(String ipurl,
671           StringBuffer warnings)
672   {
673     boolean valid = true;
674     int lastp = 0;
675     String url = new String();
676     Matcher prms = PARAM_ENCODED_URL_PATTERN
677             .matcher(ipurl);
678     Map<String, InputType> iparams = new Hashtable<String, InputType>();
679     InputType jinput;
680     while (prms.find())
681     {
682       if (lastp < prms.start(0))
683       {
684         url += ipurl.substring(lastp, prms.start(0));
685         lastp = prms.end(0) + 1;
686       }
687       String sep = prms.group(1);
688       String tok = prms.group(2);
689       String iprm = prms.group(3);
690       int colon = iprm.indexOf(":");
691       String iprmparams = "";
692       if (colon > -1)
693       {
694         iprmparams = iprm.substring(colon + 1);
695         iprm = iprm.substring(0, colon);
696       }
697       valid = parseTypeString(prms.group(0), tok, iprm, iprmparams,
698               iparams, warnings);
699     }
700     if (valid)
701     {
702       try
703       {
704         URL u = new URL(url);
705         postUrl = url;
706         inputParams = iparams;
707       } catch (Exception e)
708       {
709         warnings.append("Failed to parse '" + url + "' as a URL.\n");
710         valid = false;
711       }
712     }
713     return valid;
714   }
715
716   public static Class[] getInputTypes()
717   {
718     // TODO - find a better way of maintaining this classlist
719     return new Class[]
720     { jalview.ws.rest.params.Alignment.class,
721         jalview.ws.rest.params.AnnotationFile.class,
722         SeqGroupIndexVector.class,
723         jalview.ws.rest.params.SeqIdVector.class,
724         jalview.ws.rest.params.SeqVector.class,
725         jalview.ws.rest.params.Tree.class };
726   }
727
728   public static boolean parseTypeString(String fullstring, String tok,
729           String iprm, String iprmparams, Map<String, InputType> iparams,
730           StringBuffer warnings)
731   {
732     boolean valid = true;
733     InputType jinput;
734     for (Class type : getInputTypes())
735     {
736       try
737       {
738         jinput = (InputType) (type.getConstructor().newInstance());
739         if (iprm.equalsIgnoreCase(jinput.getURLtokenPrefix()))
740         {
741           ArrayList<String> al = new ArrayList<String>();
742           for (String prprm : StringUtils.separatorListToArray(iprmparams, ","))
743           {
744             // hack to ensure that strings like "sep=','" containing unescaped
745             // commas as values are concatenated
746             al.add(prprm.trim());
747           }
748           if (!jinput.configureFromURLtokenString(al, warnings))
749           {
750             valid = false;
751             warnings.append("Failed to parse '" + fullstring + "' as a "
752                     + jinput.getURLtokenPrefix() + " input tag.\n");
753           }
754           else
755           {
756             jinput.token = tok;
757             iparams.put(tok, jinput);
758             valid = true;
759           }
760           break;
761         }
762
763       } catch (Throwable thr)
764       {
765       }
766       ;
767     }
768     return valid;
769   }
770
771   /**
772    * covenience method to generate the id and sequence string vector from a set
773    * of seuqences using each sequence's getName() and getSequenceAsString()
774    * method
775    * 
776    * @param seqs
777    * @return String[][] {{sequence ids},{sequence strings}}
778    */
779   public static String[][] formStrings(SequenceI[] seqs)
780   {
781     String[][] idset = new String[2][seqs.length];
782     for (int i = 0; i < seqs.length; i++)
783     {
784       idset[0][i] = seqs[i].getName();
785       idset[1][i] = seqs[i].getSequenceAsString();
786     }
787     return idset;
788   }
789
790   /**
791    * can this service be run on the visible portion of an alignment regardless
792    * of hidden boundaries ?
793    */
794   boolean hseparable = false;
795
796   boolean vseparable = false;
797
798   public boolean isHseparable()
799   {
800     return hseparable;
801   }
802
803   /**
804    * 
805    * @return
806    */
807   public boolean isVseparable()
808   {
809     return vseparable;
810   }
811
812   /**
813    * search the input types for an instance of the given class
814    * 
815    * @param <validInput.inputType> class1
816    * @return
817    */
818   public boolean inputInvolves(Class<?> class1)
819   {
820     assert (InputType.class.isAssignableFrom(class1));
821     for (InputType val : inputParams.values())
822     {
823       if (class1.isAssignableFrom(val.getClass()))
824       {
825         return true;
826       }
827     }
828     return false;
829   }
830
831   char gapCharacter = '-';
832
833   /**
834    * 
835    * @return the preferred gap character for alignments input/output by this
836    *         service
837    */
838   public char getGapCharacter()
839   {
840     return gapCharacter;
841   }
842
843   public String getDecoratedResultUrl(String jobId)
844   {
845     // TODO: JPred4 - jobs are checked at a different path to the jobId ?
846     // TODO: correctly write ?/& appropriate to result URL format.
847     return jobId + urlSuffix;
848   }
849
850   private List<JvDataType> resultData = new ArrayList<JvDataType>();
851
852   /**
853    * 
854    * 
855    * TODO: Extend to optionally specify relative/absolute url where data of this
856    * type can be retrieved from
857    * 
858    * @param dt
859    */
860   public void addResultDatatype(JvDataType dt)
861   {
862     if (resultData == null)
863     {
864       resultData = new ArrayList<JvDataType>();
865     }
866     resultData.add(dt);
867   }
868
869   public boolean removeRsultDatatype(JvDataType dt)
870   {
871     if (resultData != null)
872     {
873       return resultData.remove(dt);
874     }
875     return false;
876   }
877
878   public List<JvDataType> getResultDataTypes()
879   {
880     return resultData;
881   }
882
883   /**
884    * parse a concatenated list of rest service descriptions into an array
885    * 
886    * @param services
887    * @return zero or more services.
888    * @throws exceptions
889    *           if the services are improperly encoded.
890    */
891   public static List<RestServiceDescription> parseDescriptions(
892           String services) throws Exception
893   {
894     String[] list = StringUtils.separatorListToArray(services, "|");
895     List<RestServiceDescription> svcparsed = new ArrayList<RestServiceDescription>();
896     int p = 0, lastp = 0;
897     StringBuffer warnings = new StringBuffer();
898     do
899     {
900       RestServiceDescription rsd = new RestServiceDescription();
901       p = rsd.parseServiceList(list, warnings, lastp = p);
902       if (p > lastp && rsd.isValid())
903       {
904         svcparsed.add(rsd);
905       }
906       else
907       {
908         throw new Exception(
909                 "Failed to parse user defined RSBS services from :"
910                         + services
911                         + "\nFirst error was encountered at token " + lastp
912                         + " starting " + list[lastp] + ":\n"
913                         + rsd.getInvalidMessage());
914       }
915     } while (p < lastp && p < list.length - 1);
916     return svcparsed;
917   }
918
919 }