JAL-629 move SubId to more logical place. Attempt removal of annotations in a better...
[jalview.git] / src / jalview / bin / ArgParser.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.bin;
22
23 import java.net.URLDecoder;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.EnumSet;
28 import java.util.Enumeration;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Map;
33
34 import jalview.util.Platform;
35
36 public class ArgParser
37 {
38   private static final String NEGATESTRING = "no";
39
40   private static final String DEFAULTLINKEDID = "";
41
42   private static enum Opt
43   {
44     BOOLEAN, STRING, UNARY, MULTI, LINKED, ORDERED
45   }
46
47   public enum Arg
48   {
49     /*
50     NOCALCULATION, NOMENUBAR, NOSTATUS, SHOWOVERVIEW, ANNOTATIONS, COLOUR,
51     FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, NOANNOTATION, NOANNOTATION2,
52     NODISPLAY, NOGUI, NONEWS, NOQUESTIONNAIRE, NOSORTBYTREE, NOUSAGESTATS,
53     OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, SORTBYTREE, TREE, VDOC,
54     VSESS;
55     */
56     HELP("h"), CALCULATION, MENUBAR, STATUS, SHOWOVERVIEW, ANNOTATIONS,
57     COLOUR, FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, ANNOTATION,
58     ANNOTATION2, DISPLAY, GUI, NEWS, NOQUESTIONNAIRE, SORTBYTREE,
59     USAGESTATS, OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, TREE, VDOC,
60     VSESS, OUTPUT, OUTPUTTYPE, SSANNOTATION, NOTEMPFAC, TEMPFAC,
61     TEMPFAC_LABEL, TEMPFAC_DESC, TEMPFAC_SHADING, TITLE, PAEMATRIX, WRAP,
62     NOSTRUCTURE, STRUCTURE;
63
64     static
65     {
66       HELP.setOptions(Opt.UNARY);
67       CALCULATION.setOptions(true, Opt.BOOLEAN); // default "true" implies only
68                                                  // expecting "--nocalculation"
69       MENUBAR.setOptions(true, Opt.BOOLEAN);
70       STATUS.setOptions(true, Opt.BOOLEAN);
71       SHOWOVERVIEW.setOptions(Opt.UNARY, Opt.LINKED);
72       ANNOTATIONS.setOptions(Opt.STRING, Opt.LINKED);
73       COLOUR.setOptions(Opt.STRING, Opt.LINKED);
74       FEATURES.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
75       GROOVY.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
76       GROUPS.setOptions(Opt.STRING, Opt.LINKED);
77       HEADLESS.setOptions(Opt.UNARY);
78       JABAWS.setOptions(Opt.STRING);
79       ANNOTATION.setOptions(true, Opt.BOOLEAN);
80       ANNOTATION2.setOptions(true, Opt.BOOLEAN);
81       DISPLAY.setOptions(true, Opt.BOOLEAN);
82       GUI.setOptions(true, Opt.BOOLEAN);
83       NEWS.setOptions(true, Opt.BOOLEAN);
84       NOQUESTIONNAIRE.setOptions(Opt.UNARY); // unary as --questionnaire=val
85                                              // expects a string value
86       SORTBYTREE.setOptions(true, Opt.BOOLEAN);
87       USAGESTATS.setOptions(true, Opt.BOOLEAN);
88       OPEN.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
89       OPEN2.setOptions(Opt.STRING, Opt.LINKED);
90       PROPS.setOptions(Opt.STRING);
91       QUESTIONNAIRE.setOptions(Opt.STRING);
92       SETPROP.setOptions(Opt.STRING);
93       TREE.setOptions(Opt.STRING);
94
95       VDOC.setOptions(Opt.UNARY);
96       VSESS.setOptions(Opt.UNARY);
97
98       OUTPUT.setOptions(Opt.STRING, Opt.LINKED);
99       OUTPUTTYPE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
100
101       SSANNOTATION.setOptions(Opt.BOOLEAN, Opt.LINKED);
102       NOTEMPFAC.setOptions(Opt.UNARY, Opt.LINKED);
103       TEMPFAC.setOptions(Opt.STRING, Opt.LINKED);
104       TEMPFAC_LABEL.setOptions(Opt.STRING, Opt.LINKED);
105       TEMPFAC_DESC.setOptions(Opt.STRING, Opt.LINKED);
106       TEMPFAC_SHADING.setOptions(Opt.STRING, Opt.LINKED);
107       TITLE.setOptions(Opt.STRING, Opt.LINKED);
108       PAEMATRIX.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
109       NOSTRUCTURE.setOptions(Opt.UNARY, Opt.LINKED);
110       STRUCTURE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
111       WRAP.setOptions(Opt.BOOLEAN, Opt.LINKED);
112     }
113
114     private final String[] argNames;
115
116     private Opt[] argOptions;
117
118     private boolean defaultBoolValue = false;
119
120     public String toLongString()
121     {
122       StringBuilder sb = new StringBuilder();
123       sb.append("Arg: ").append(this.name());
124       for (String name : getNames())
125       {
126         sb.append(", '").append(name).append("'");
127       }
128       sb.append("\nOptions: ");
129       boolean first = true;
130       for (Opt o : argOptions)
131       {
132         if (!first)
133         {
134           sb.append(", ");
135         }
136         sb.append(o.toString());
137         first = false;
138       }
139       sb.append("\n");
140       return sb.toString();
141     }
142
143     private Arg()
144     {
145       this(new String[0]);
146     }
147
148     private Arg(String... names)
149     {
150       int length = (names == null || names.length == 0
151               || (names.length == 1 && names[0] == null)) ? 1
152                       : names.length + 1;
153       this.argNames = new String[length];
154       this.argNames[0] = this.getName();
155       if (length > 1)
156         System.arraycopy(names, 0, this.argNames, 1, names.length);
157     }
158
159     public String[] getNames()
160     {
161       return argNames;
162     }
163
164     public String getName()
165     {
166       return this.name().toLowerCase(Locale.ROOT).replace('_', '-');
167     }
168
169     @Override
170     public final String toString()
171     {
172       return getName();
173     }
174
175     public boolean hasOption(Opt o)
176     {
177       if (argOptions == null)
178         return false;
179       for (Opt option : argOptions)
180       {
181         if (o == option)
182           return true;
183       }
184       return false;
185     }
186
187     protected void setOptions(Opt... options)
188     {
189       setOptions(false, options);
190     }
191
192     protected void setOptions(boolean defaultBoolValue, Opt... options)
193     {
194       this.defaultBoolValue = defaultBoolValue;
195       argOptions = options;
196     }
197
198     protected boolean getDefaultBoolValue()
199     {
200       return defaultBoolValue;
201     }
202   }
203
204   public static class ArgValues
205   {
206     private Arg arg;
207
208     private int argCount = 0;
209
210     private boolean boolValue = false;
211
212     private boolean negated = false;
213
214     private List<String> argsList;
215
216     protected ArgValues(Arg a)
217     {
218       this.arg = a;
219       this.argsList = new ArrayList<String>();
220       this.boolValue = arg.getDefaultBoolValue();
221     }
222
223     public Arg arg()
224     {
225       return arg;
226     }
227
228     protected int getCount()
229     {
230       return argCount;
231     }
232
233     protected void incrementCount()
234     {
235       argCount++;
236     }
237
238     protected void setNegated(boolean b)
239     {
240       this.negated = b;
241     }
242
243     protected boolean isNegated()
244     {
245       return this.negated;
246     }
247
248     protected void setBoolean(boolean b)
249     {
250       this.boolValue = b;
251     }
252
253     protected boolean getBoolean()
254     {
255       return this.boolValue;
256     }
257
258     @Override
259     public String toString()
260     {
261       if (argsList == null)
262         return null;
263       StringBuilder sb = new StringBuilder();
264       sb.append(arg.toLongString());
265       if (arg.hasOption(Opt.BOOLEAN) || arg.hasOption(Opt.UNARY))
266         sb.append("Boolean: ").append(boolValue).append("; Default: ")
267                 .append(arg.getDefaultBoolValue()).append("; Negated: ")
268                 .append(negated).append("\n");
269       if (arg.hasOption(Opt.STRING))
270       {
271         sb.append("Values:");
272         boolean first = true;
273         for (String v : argsList)
274         {
275           if (!first)
276             sb.append(",");
277           sb.append("\n  '");
278           sb.append(v).append("'");
279           first = false;
280         }
281         sb.append("\n");
282       }
283       sb.append("Count: ").append(argCount).append("\n");
284       return sb.toString();
285     }
286
287     protected void addValue()
288     {
289       addValue(null);
290     }
291
292     protected void addValue(String val)
293     {
294       addValue(val, false);
295     }
296
297     protected void addValue(String val, boolean noDuplicates)
298     {
299       if ((!arg.hasOption(Opt.MULTI) && argsList.size() > 0)
300               || (noDuplicates && argsList.contains(val)))
301         return;
302       if (argsList == null)
303       {
304         Console.warn("** inst");
305         argsList = new ArrayList<String>();
306       }
307       argsList.add(val);
308     }
309
310     protected boolean hasValue(String val)
311     {
312       return argsList.contains(val);
313     }
314
315     protected String getValue()
316     {
317       if (arg.hasOption(Opt.MULTI))
318         Console.warn("Requesting single value for multi value argument");
319       return argsList.size() > 0 ? argsList.get(0) : null;
320     }
321
322     protected List<String> getValues()
323     {
324       return argsList;
325     }
326   }
327
328   // old style
329   private List<String> vargs = null;
330
331   private boolean isApplet;
332
333   // private AppletParams appletParams;
334
335   public boolean isApplet()
336   {
337     return isApplet;
338   }
339
340   public String nextValue()
341   {
342     return vargs.remove(0);
343   }
344
345   public int getSize()
346   {
347     return vargs.size();
348   }
349
350   public String getValue(String arg)
351   {
352     return getValue(arg, false);
353   }
354
355   public String getValue(String arg, boolean utf8decode)
356   {
357     int index = vargs.indexOf(arg);
358     String dc = null, ret = null;
359     if (index != -1)
360     {
361       ret = vargs.get(index + 1).toString();
362       vargs.remove(index);
363       vargs.remove(index);
364       if (utf8decode && ret != null)
365       {
366         try
367         {
368           dc = URLDecoder.decode(ret, "UTF-8");
369           ret = dc;
370         } catch (Exception e)
371         {
372           // TODO: log failure to decode
373         }
374       }
375     }
376     return ret;
377   }
378
379   /*
380   public Object getAppletValue(String key, String def, boolean asString)
381   {
382     Object value;
383     return (appletParams == null ? null
384             : (value = appletParams.get(key.toLowerCase())) == null ? def
385                     : asString ? "" + value : value);
386   }
387   */
388
389   // new style
390   private static final Map<String, Arg> argMap;
391
392   private Map<String, HashMap<Arg, ArgValues>> linkedArgs = new HashMap<>();
393
394   private List<String> linkedOrder = null;
395
396   private List<Arg> argList;
397
398   static
399   {
400     argMap = new HashMap<>();
401     for (Arg a : EnumSet.allOf(Arg.class))
402     {
403       ARGNAME: for (String argName : a.getNames())
404       {
405         if (argMap.containsKey(argName))
406         {
407           Console.warn("Trying to add argument name multiple times: '"
408                   + argName + "'"); // RESTORE THIS WHEN MERGED
409           if (argMap.get(argName) != a)
410           {
411             Console.error(
412                     "Trying to add argument name multiple times for different Args: '"
413                             + argMap.get(argName).getName() + ":" + argName
414                             + "' and '" + a.getName() + ":" + argName
415                             + "'");
416           }
417           continue ARGNAME;
418         }
419         argMap.put(argName, a);
420       }
421     }
422   }
423
424   public ArgParser(String[] args)
425   {
426     // old style
427     vargs = new ArrayList<>();
428     isApplet = (args.length > 0 && args[0].startsWith("<applet"));
429     if (isApplet)
430     {
431       // appletParams = AppletParams.getAppletParams(args, vargs);
432     }
433     else
434     {
435       if (Platform.isJS())
436
437       {
438         isApplet = true;
439         // appletParams =
440         // AppletParams.getAppletParams(Platform.getAppletInfoAsMap(), vargs);
441       }
442       for (int i = 0; i < args.length; i++)
443       {
444         String arg = args[i].trim();
445         if (arg.charAt(0) == '-')
446         {
447           arg = arg.substring(1);
448         }
449         vargs.add(arg);
450       }
451     }
452
453     // new style
454     Enumeration<String> argE = Collections.enumeration(Arrays.asList(args));
455     ARG: while (argE.hasMoreElements())
456     {
457       String arg = argE.nextElement();
458       String argName = null;
459       String val = null;
460       String linkedId = null;
461       if (arg.startsWith("--"))
462       {
463         int equalPos = arg.indexOf('=');
464         if (equalPos > -1)
465         {
466           argName = arg.substring(2, equalPos);
467           val = arg.substring(equalPos + 1);
468         }
469         else
470         {
471           argName = arg.substring(2);
472         }
473         int idOpen = argName.indexOf('[');
474         int idClose = argName.indexOf(']');
475
476         if (idOpen > -1 && idClose == argName.length() - 1)
477         {
478           linkedId = argName.substring(idOpen + 1, idClose);
479           argName = argName.substring(0, idOpen);
480         }
481
482         Arg a = argMap.get(argName);
483         // check for boolean prepended by "no"
484         boolean negated = false;
485         if (a == null && argName.startsWith(NEGATESTRING) && argMap
486                 .containsKey(argName.substring(NEGATESTRING.length())))
487         {
488           argName = argName.substring(NEGATESTRING.length());
489           a = argMap.get(argName);
490           negated = true;
491         }
492
493         // check for config errors
494         if (a == null)
495         {
496           // arg not found
497           Console.error("Argument '" + arg + "' not recognised. Ignoring.");
498           continue ARG;
499         }
500         if (!a.hasOption(Opt.BOOLEAN) && negated)
501         {
502           // used "no" with a non-boolean option
503           Console.error("Argument '--" + NEGATESTRING + argName
504                   + "' not a boolean option. Ignoring.");
505           continue ARG;
506         }
507         if (!a.hasOption(Opt.STRING) && equalPos > -1)
508         {
509           // set --argname=value when arg does not accept values
510           Console.error("Argument '--" + argName
511                   + "' does not expect a value (given as '" + arg
512                   + "').  Ignoring.");
513           continue ARG;
514         }
515         if (!a.hasOption(Opt.LINKED) && linkedId != null)
516         {
517           // set --argname[linkedId] when arg does not use linkedIds
518           Console.error("Argument '--" + argName
519                   + "' does not expect a linked id (given as '" + arg
520                   + "'). Ignoring.");
521           continue ARG;
522         }
523
524         if (a.hasOption(Opt.STRING) && equalPos == -1)
525         {
526           // take next arg as value if required, and '=' was not found
527           if (!argE.hasMoreElements())
528           {
529             // no value to take for arg, which wants a value
530             Console.error("Argument '" + a.getName()
531                     + "' requires a value, none given. Ignoring.");
532             continue ARG;
533           }
534           val = argE.nextElement();
535         }
536
537         // use default linkedId for linked arguments
538         if (a.hasOption(Opt.LINKED) && linkedId == null)
539           linkedId = DEFAULTLINKEDID;
540
541         if (!linkedArgs.containsKey(linkedId))
542           linkedArgs.put(linkedId, new HashMap<>());
543
544         Map<Arg, ArgValues> valuesMap = linkedArgs.get(linkedId);
545         if (!valuesMap.containsKey(a))
546           valuesMap.put(a, new ArgValues(a));
547
548         ArgValues values = valuesMap.get(a);
549         if (values == null)
550         {
551           values = new ArgValues(a);
552         }
553         // store appropriate value
554         if (a.hasOption(Opt.STRING))
555         {
556           values.addValue(val);
557         }
558         else if (a.hasOption(Opt.BOOLEAN))
559         {
560           values.setBoolean(!negated);
561           values.setNegated(negated);
562         }
563         else if (a.hasOption(Opt.UNARY))
564         {
565           values.setBoolean(true);
566         }
567         values.incrementCount();
568
569         // store in appropriate place
570         if (a.hasOption(Opt.LINKED))
571         {
572           // allow a default linked id for single usage
573           if (linkedId == null)
574             linkedId = DEFAULTLINKEDID;
575           // store the order of linkedIds
576           if (linkedOrder == null)
577             linkedOrder = new ArrayList<>();
578           if (!linkedOrder.contains(linkedId))
579             linkedOrder.add(linkedId);
580         }
581         // store the ArgValues
582         valuesMap.put(a, values);
583
584         // store arg in the list of args
585         if (argList == null)
586           argList = new ArrayList<>();
587         if (!argList.contains(a))
588           argList.add(a);
589       }
590     }
591   }
592
593   public boolean isSet(Arg a)
594   {
595     return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
596   }
597
598   public boolean isSet(String linkedId, Arg a)
599   {
600     Map<Arg, ArgValues> m = linkedArgs.get(linkedId);
601     return m == null ? false : m.containsKey(a);
602   }
603
604   public boolean getBool(Arg a)
605   {
606     if (!a.hasOption(Opt.BOOLEAN))
607     {
608       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
609               + "'.");
610     }
611     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
612   }
613
614   public boolean getBool(String linkedId, Arg a)
615   {
616     Map<Arg, ArgValues> m = linkedArgs.get(linkedId);
617     if (m == null)
618       return a.getDefaultBoolValue();
619     ArgValues v = m.get(a);
620     return v == null ? a.getDefaultBoolValue() : v.getBoolean();
621   }
622
623   public List<String> linkedIds()
624   {
625     return linkedOrder;
626   }
627
628   public HashMap<Arg, ArgValues> linkedArgs(String id)
629   {
630     return linkedArgs.get(id);
631   }
632
633   @Override
634   public String toString()
635   {
636     StringBuilder sb = new StringBuilder();
637     sb.append("UNLINKED\n");
638     sb.append(argMapToString(linkedArgs.get(null)));
639     if (linkedIds() != null)
640     {
641       sb.append("LINKED\n");
642       for (String id : linkedIds())
643       {
644         // already listed these as UNLINKED args
645         if (id == null)
646           continue;
647
648         Map<Arg, ArgValues> m = linkedArgs(id);
649         sb.append("ID: '").append(id).append("'\n");
650         sb.append(argMapToString(m));
651       }
652     }
653     return sb.toString();
654   }
655
656   private static String argMapToString(Map<Arg, ArgValues> m)
657   {
658     if (m == null)
659       return null;
660     StringBuilder sb = new StringBuilder();
661     for (Arg a : m.keySet())
662     {
663       ArgValues v = m.get(a);
664       sb.append(v.toString());
665       sb.append("\n");
666     }
667     return sb.toString();
668   }
669
670   public static SubId getSubId(String item)
671   {
672     return new SubId(item);
673   }
674
675   /**
676    * A helper class to parse a string of the possible forms "content"
677    * "[index]content", "[keyName=keyValue]content" and return the integer index,
678    * the strings keyName and keyValue, and the content after the square brackets
679    * (if present). Values not set `will be -1 or null.
680    */
681   public static class SubId
682   {
683     protected int index = 0;
684
685     protected String keyName = null;
686
687     protected String keyValue = null;
688
689     protected String content = null;
690
691     public SubId()
692     {
693     }
694
695     public SubId(String item)
696     {
697       this.parseVal(item);
698     }
699
700     public void parseVal(String item)
701     {
702       if (item.indexOf('[') == 0 && item.indexOf(']') > 1)
703       {
704         int openBracket = item.indexOf('[');
705         int closeBracket = item.indexOf(']');
706         String indexString = item.substring(openBracket + 1, closeBracket);
707         this.content = item.substring(closeBracket + 1);
708         int equals = indexString.indexOf('=');
709         if (equals > -1)
710         {
711           this.keyName = indexString.substring(0, equals);
712           this.keyValue = indexString.substring(equals + 1);
713           this.index = -1;
714         }
715         else
716         {
717           try
718           {
719             this.index = Integer.parseInt(indexString);
720           } catch (NumberFormatException e)
721           {
722             Console.warn("Failed to obtain sequenced id or index from '"
723                     + item + "'. Setting index=0 and using content='"
724                     + content + "'.");
725           }
726         }
727       }
728       else
729       {
730         this.content = item;
731       }
732     }
733   }
734 }