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