cc31f5bc8715eff4697b6c1963117be6308195b3
[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.Collection;
27 import java.util.Collections;
28 import java.util.EnumSet;
29 import java.util.Enumeration;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.Set;
35
36 import jalview.util.Platform;
37
38 public class ArgParser
39 {
40   private static final String NEGATESTRING = "no";
41
42   private static final String DEFAULTLINKEDID = "";
43
44   private static enum Opt
45   {
46     BOOLEAN, STRING, UNARY, MULTI, LINKED, ORDERED, NODUPLICATEVALUES
47   }
48
49   public enum Arg
50   {
51     /*
52     NOCALCULATION, NOMENUBAR, NOSTATUS, SHOWOVERVIEW, ANNOTATIONS, COLOUR,
53     FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, NOANNOTATION, NOANNOTATION2,
54     NODISPLAY, NOGUI, NONEWS, NOQUESTIONNAIRE, NOSORTBYTREE, NOUSAGESTATS,
55     OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, SORTBYTREE, TREE, VDOC,
56     VSESS;
57     */
58     HELP("h"), CALCULATION, MENUBAR, STATUS, SHOWOVERVIEW, ANNOTATIONS,
59     COLOUR, FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, ANNOTATION,
60     ANNOTATION2, DISPLAY, GUI, NEWS, NOQUESTIONNAIRE, SORTBYTREE,
61     USAGESTATS, OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, TREE, VDOC,
62     VSESS, OUTPUT, OUTPUTTYPE, SSANNOTATION, NOTEMPFAC, TEMPFAC,
63     TEMPFAC_LABEL, TEMPFAC_DESC, TEMPFAC_SHADING, TITLE, PAEMATRIX, WRAP,
64     NOSTRUCTURE, STRUCTURE, IMAGE, QUIT, DEBUG("d");
65
66     static
67     {
68       HELP.setOptions(Opt.UNARY);
69       CALCULATION.setOptions(true, Opt.BOOLEAN); // default "true" implies only
70                                                  // expecting "--nocalculation"
71       MENUBAR.setOptions(true, Opt.BOOLEAN);
72       STATUS.setOptions(true, Opt.BOOLEAN);
73       SHOWOVERVIEW.setOptions(Opt.UNARY, Opt.LINKED);
74       ANNOTATIONS.setOptions(Opt.STRING, Opt.LINKED);
75       COLOUR.setOptions(Opt.STRING, Opt.LINKED);
76       FEATURES.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
77       GROOVY.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
78       GROUPS.setOptions(Opt.STRING, Opt.LINKED);
79       HEADLESS.setOptions(Opt.UNARY);
80       JABAWS.setOptions(Opt.STRING);
81       ANNOTATION.setOptions(true, Opt.BOOLEAN);
82       ANNOTATION2.setOptions(true, Opt.BOOLEAN);
83       DISPLAY.setOptions(true, Opt.BOOLEAN);
84       GUI.setOptions(true, Opt.BOOLEAN);
85       NEWS.setOptions(true, Opt.BOOLEAN);
86       NOQUESTIONNAIRE.setOptions(Opt.UNARY); // unary as --questionnaire=val
87                                              // expects a string value
88       SORTBYTREE.setOptions(true, Opt.BOOLEAN);
89       USAGESTATS.setOptions(true, Opt.BOOLEAN);
90       OPEN.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
91       OPEN2.setOptions(Opt.STRING, Opt.LINKED);
92       PROPS.setOptions(Opt.STRING);
93       QUESTIONNAIRE.setOptions(Opt.STRING);
94       SETPROP.setOptions(Opt.STRING);
95       TREE.setOptions(Opt.STRING);
96
97       VDOC.setOptions(Opt.UNARY);
98       VSESS.setOptions(Opt.UNARY);
99
100       OUTPUT.setOptions(Opt.STRING, Opt.LINKED);
101       OUTPUTTYPE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
102
103       SSANNOTATION.setOptions(Opt.BOOLEAN, Opt.LINKED);
104       NOTEMPFAC.setOptions(Opt.UNARY, Opt.LINKED);
105       TEMPFAC.setOptions(Opt.STRING, Opt.LINKED);
106       TEMPFAC_LABEL.setOptions(Opt.STRING, Opt.LINKED);
107       TEMPFAC_DESC.setOptions(Opt.STRING, Opt.LINKED);
108       TEMPFAC_SHADING.setOptions(Opt.BOOLEAN, Opt.LINKED);
109       TITLE.setOptions(Opt.STRING, Opt.LINKED);
110       PAEMATRIX.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
111       NOSTRUCTURE.setOptions(Opt.UNARY, Opt.LINKED);
112       STRUCTURE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
113       WRAP.setOptions(Opt.BOOLEAN, Opt.LINKED);
114       IMAGE.setOptions(Opt.STRING, Opt.LINKED);
115       QUIT.setOptions(Opt.UNARY);
116       DEBUG.setOptions(Opt.BOOLEAN);
117     }
118
119     private final String[] argNames;
120
121     private Opt[] argOptions;
122
123     private boolean defaultBoolValue = false;
124
125     public String toLongString()
126     {
127       StringBuilder sb = new StringBuilder();
128       sb.append("Arg: ").append(this.name());
129       for (String name : getNames())
130       {
131         sb.append(", '").append(name).append("'");
132       }
133       sb.append("\nOptions: ");
134       boolean first = true;
135       for (Opt o : argOptions)
136       {
137         if (!first)
138         {
139           sb.append(", ");
140         }
141         sb.append(o.toString());
142         first = false;
143       }
144       sb.append("\n");
145       return sb.toString();
146     }
147
148     private Arg()
149     {
150       this(new String[0]);
151     }
152
153     private Arg(String... names)
154     {
155       int length = (names == null || names.length == 0
156               || (names.length == 1 && names[0] == null)) ? 1
157                       : names.length + 1;
158       this.argNames = new String[length];
159       this.argNames[0] = this.getName();
160       if (length > 1)
161         System.arraycopy(names, 0, this.argNames, 1, names.length);
162     }
163
164     public String[] getNames()
165     {
166       return argNames;
167     }
168
169     public String getName()
170     {
171       return this.name().toLowerCase(Locale.ROOT).replace('_', '-');
172     }
173
174     @Override
175     public final String toString()
176     {
177       return getName();
178     }
179
180     public boolean hasOption(Opt o)
181     {
182       if (argOptions == null)
183         return false;
184       for (Opt option : argOptions)
185       {
186         if (o == option)
187           return true;
188       }
189       return false;
190     }
191
192     protected void setOptions(Opt... options)
193     {
194       setOptions(false, options);
195     }
196
197     protected void setOptions(boolean defaultBoolValue, Opt... options)
198     {
199       this.defaultBoolValue = defaultBoolValue;
200       argOptions = options;
201     }
202
203     protected boolean getDefaultBoolValue()
204     {
205       return defaultBoolValue;
206     }
207   }
208
209   public static class ArgValues
210   {
211     private static final String ID = "id";
212
213     private Arg arg;
214
215     private int argCount = 0;
216
217     private boolean boolValue = false;
218
219     private boolean negated = false;
220
221     private int boolIndex = -1;
222
223     private List<Integer> argsIndexes;
224
225     private List<ArgValue> argValueList;
226
227     private Map<String, ArgValue> idMap = new HashMap<>();
228
229     protected ArgValues(Arg a)
230     {
231       this.arg = a;
232       this.argValueList = new ArrayList<ArgValue>();
233       this.boolValue = arg.getDefaultBoolValue();
234     }
235
236     public Arg arg()
237     {
238       return arg;
239     }
240
241     protected int getCount()
242     {
243       return argCount;
244     }
245
246     protected void incrementCount()
247     {
248       argCount++;
249     }
250
251     protected void setNegated(boolean b)
252     {
253       this.negated = b;
254     }
255
256     protected boolean isNegated()
257     {
258       return this.negated;
259     }
260
261     protected void setBoolean(boolean b, int i)
262     {
263       this.boolValue = b;
264       this.boolIndex = i;
265     }
266
267     protected boolean getBoolean()
268     {
269       return this.boolValue;
270     }
271
272     @Override
273     public String toString()
274     {
275       if (argValueList == null)
276         return null;
277       StringBuilder sb = new StringBuilder();
278       sb.append(arg.toLongString());
279       if (arg.hasOption(Opt.BOOLEAN) || arg.hasOption(Opt.UNARY))
280         sb.append("Boolean: ").append(boolValue).append("; Default: ")
281                 .append(arg.getDefaultBoolValue()).append("; Negated: ")
282                 .append(negated).append("\n");
283       if (arg.hasOption(Opt.STRING))
284       {
285         sb.append("Values:");
286         boolean first = true;
287         for (ArgValue av : argValueList)
288         {
289           String v = av.getValue();
290           if (!first)
291             sb.append(",");
292           sb.append("\n  '");
293           sb.append(v).append("'");
294           first = false;
295         }
296         sb.append("\n");
297       }
298       sb.append("Count: ").append(argCount).append("\n");
299       return sb.toString();
300     }
301
302     protected void addValue()
303     {
304       addValue(null, -1);
305     }
306
307     protected void addValue(String val, int argIndex)
308     {
309       addArgValue(new ArgValue(val, argIndex));
310     }
311
312     protected void addArgValue(ArgValue av)
313     {
314       if ((!arg.hasOption(Opt.MULTI) && argValueList.size() > 0)
315               || (arg.hasOption(Opt.NODUPLICATEVALUES)
316                       && argValueList.contains(av.getValue())))
317         return;
318       if (argValueList == null)
319       {
320         argValueList = new ArrayList<ArgValue>();
321       }
322       SubVals sv = ArgParser.getSubVals(av.getValue());
323       if (sv.has(ID))
324       {
325         String id = sv.get(ID);
326         av.setId(id);
327         idMap.put(id, av);
328       }
329       argValueList.add(av);
330     }
331
332     protected boolean hasValue(String val)
333     {
334       return argValueList.contains(val);
335     }
336
337     protected ArgValue getArgValue()
338     {
339       if (arg.hasOption(Opt.MULTI))
340         Console.warn("Requesting single value for multi value argument");
341       return argValueList.size() > 0 ? argValueList.get(0) : null;
342     }
343
344     protected List<ArgValue> getArgValueList()
345     {
346       return argValueList;
347     }
348
349     protected boolean hasId(String id)
350     {
351       return idMap.containsKey(id);
352     }
353
354     protected ArgValue getId(String id)
355     {
356       return idMap.get(id);
357     }
358   }
359
360   // old style
361   private List<String> vargs = null;
362
363   private boolean isApplet;
364
365   // private AppletParams appletParams;
366
367   public boolean isApplet()
368   {
369     return isApplet;
370   }
371
372   public String nextValue()
373   {
374     return vargs.remove(0);
375   }
376
377   public int getSize()
378   {
379     return vargs.size();
380   }
381
382   public String getValue(String arg)
383   {
384     return getValue(arg, false);
385   }
386
387   public String getValue(String arg, boolean utf8decode)
388   {
389     int index = vargs.indexOf(arg);
390     String dc = null;
391     String ret = null;
392     if (index != -1)
393     {
394       ret = vargs.get(index + 1).toString();
395       vargs.remove(index);
396       vargs.remove(index);
397       if (utf8decode && ret != null)
398       {
399         try
400         {
401           dc = URLDecoder.decode(ret, "UTF-8");
402           ret = dc;
403         } catch (Exception e)
404         {
405           // TODO: log failure to decode
406         }
407       }
408     }
409     return ret;
410   }
411
412   /*
413   public Object getAppletValue(String key, String def, boolean asString)
414   {
415     Object value;
416     return (appletParams == null ? null
417             : (value = appletParams.get(key.toLowerCase())) == null ? def
418                     : asString ? "" + value : value);
419   }
420   */
421
422   // new style
423   private static final Map<String, Arg> argMap;
424
425   private Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
426
427   private List<String> linkedOrder = null;
428
429   private List<Arg> argList;
430
431   static
432   {
433     argMap = new HashMap<>();
434     for (Arg a : EnumSet.allOf(Arg.class))
435     {
436       ARGNAME: for (String argName : a.getNames())
437       {
438         if (argMap.containsKey(argName))
439         {
440           Console.warn("Trying to add argument name multiple times: '"
441                   + argName + "'"); // RESTORE THIS WHEN MERGED
442           if (argMap.get(argName) != a)
443           {
444             Console.error(
445                     "Trying to add argument name multiple times for different Args: '"
446                             + argMap.get(argName).getName() + ":" + argName
447                             + "' and '" + a.getName() + ":" + argName
448                             + "'");
449           }
450           continue ARGNAME;
451         }
452         argMap.put(argName, a);
453       }
454     }
455   }
456
457   public ArgParser(String[] args)
458   {
459     // old style
460     vargs = new ArrayList<>();
461     isApplet = (args.length > 0 && args[0].startsWith("<applet"));
462     if (isApplet)
463     {
464       // appletParams = AppletParams.getAppletParams(args, vargs);
465     }
466     else
467     {
468       if (Platform.isJS())
469
470       {
471         isApplet = true;
472         // appletParams =
473         // AppletParams.getAppletParams(Platform.getAppletInfoAsMap(), vargs);
474       }
475       for (int i = 0; i < args.length; i++)
476       {
477         String arg = args[i].trim();
478         if (arg.charAt(0) == '-')
479         {
480           arg = arg.substring(1);
481         }
482         vargs.add(arg);
483       }
484     }
485
486     // new style
487     Enumeration<String> argE = Collections.enumeration(Arrays.asList(args));
488     int argIndex = 0;
489     while (argE.hasMoreElements())
490     {
491       String arg = argE.nextElement();
492       String argName = null;
493       String val = null;
494       String linkedId = null;
495       if (arg.startsWith("--"))
496       {
497         int equalPos = arg.indexOf('=');
498         if (equalPos > -1)
499         {
500           argName = arg.substring(2, equalPos);
501           val = arg.substring(equalPos + 1);
502         }
503         else
504         {
505           argName = arg.substring(2);
506         }
507         int idOpen = argName.indexOf('[');
508         int idClose = argName.indexOf(']');
509
510         if (idOpen > -1 && idClose == argName.length() - 1)
511         {
512           linkedId = argName.substring(idOpen + 1, idClose);
513           argName = argName.substring(0, idOpen);
514         }
515
516         Arg a = argMap.get(argName);
517         // check for boolean prepended by "no"
518         boolean negated = false;
519         if (a == null && argName.startsWith(NEGATESTRING) && argMap
520                 .containsKey(argName.substring(NEGATESTRING.length())))
521         {
522           argName = argName.substring(NEGATESTRING.length());
523           a = argMap.get(argName);
524           negated = true;
525         }
526
527         // check for config errors
528         if (a == null)
529         {
530           // arg not found
531           Console.error("Argument '" + arg + "' not recognised. Ignoring.");
532           continue;
533         }
534         if (!a.hasOption(Opt.BOOLEAN) && negated)
535         {
536           // used "no" with a non-boolean option
537           Console.error("Argument '--" + NEGATESTRING + argName
538                   + "' not a boolean option. Ignoring.");
539           continue;
540         }
541         if (!a.hasOption(Opt.STRING) && equalPos > -1)
542         {
543           // set --argname=value when arg does not accept values
544           Console.error("Argument '--" + argName
545                   + "' does not expect a value (given as '" + arg
546                   + "').  Ignoring.");
547           continue;
548         }
549         if (!a.hasOption(Opt.LINKED) && linkedId != null)
550         {
551           // set --argname[linkedId] when arg does not use linkedIds
552           Console.error("Argument '--" + argName
553                   + "' does not expect a linked id (given as '" + arg
554                   + "'). Ignoring.");
555           continue;
556         }
557
558         if (a.hasOption(Opt.STRING) && equalPos == -1)
559         {
560           // take next arg as value if required, and '=' was not found
561           if (!argE.hasMoreElements())
562           {
563             // no value to take for arg, which wants a value
564             Console.error("Argument '" + a.getName()
565                     + "' requires a value, none given. Ignoring.");
566             continue;
567           }
568           val = argE.nextElement();
569         }
570
571         // use default linkedId for linked arguments
572         if (a.hasOption(Opt.LINKED) && linkedId == null)
573           linkedId = DEFAULTLINKEDID;
574
575         if (!linkedArgs.containsKey(linkedId))
576           linkedArgs.put(linkedId, new ArgValuesMap());
577
578         ArgValuesMap avm = linkedArgs.get(linkedId);
579
580         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
581         {
582           Console.error("Argument '--" + argName
583                   + "' cannot contain a duplicate value ('" + val
584                   + "'). Ignoring this and subsequent occurrences.");
585           continue;
586         }
587
588         // check for unique id
589         SubVals sv = ArgParser.getSubVals(val);
590         String id = sv.get(ArgValues.ID);
591         if (id != null && avm.hasId(a, id))
592         {
593           Console.error("Argument '--" + argName + "' has a duplicate id ('"
594                   + id + "'). Ignoring.");
595           continue;
596         }
597
598         ArgValues avs = avm.getOrCreateArgValues(a);
599         if (avs == null)
600         {
601           avs = new ArgValues(a);
602         }
603         // store appropriate value
604         if (a.hasOption(Opt.STRING))
605         {
606           avs.addValue(val, argIndex);
607         }
608         else if (a.hasOption(Opt.BOOLEAN))
609         {
610           avs.setBoolean(!negated, argIndex);
611           avs.setNegated(negated);
612         }
613         else if (a.hasOption(Opt.UNARY))
614         {
615           avs.setBoolean(true, argIndex);
616         }
617         avs.incrementCount();
618
619         // store in appropriate place
620         if (a.hasOption(Opt.LINKED))
621         {
622           // allow a default linked id for single usage
623           if (linkedId == null)
624             linkedId = DEFAULTLINKEDID;
625           // store the order of linkedIds
626           if (linkedOrder == null)
627             linkedOrder = new ArrayList<>();
628           if (!linkedOrder.contains(linkedId))
629             linkedOrder.add(linkedId);
630         }
631
632         // store arg in the list of args used
633         if (argList == null)
634           argList = new ArrayList<>();
635         if (!argList.contains(a))
636           argList.add(a);
637       }
638     }
639   }
640
641   public boolean isSet(Arg a)
642   {
643     return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
644   }
645
646   public boolean isSet(String linkedId, Arg a)
647   {
648     ArgValuesMap avm = linkedArgs.get(linkedId);
649     return avm == null ? false : avm.containsArg(a);
650   }
651
652   public boolean getBool(Arg a)
653   {
654     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
655     {
656       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
657               + "'.");
658     }
659     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
660   }
661
662   public boolean getBool(String linkedId, Arg a)
663   {
664     ArgValuesMap avm = linkedArgs.get(linkedId);
665     if (avm == null)
666       return a.getDefaultBoolValue();
667     ArgValues avs = avm.getArgValues(a);
668     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
669   }
670
671   public List<String> linkedIds()
672   {
673     return linkedOrder;
674   }
675
676   public ArgValuesMap linkedArgs(String id)
677   {
678     return linkedArgs.get(id);
679   }
680
681   @Override
682   public String toString()
683   {
684     StringBuilder sb = new StringBuilder();
685     sb.append("UNLINKED\n");
686     sb.append(argValuesMapToString(linkedArgs.get(null)));
687     if (linkedIds() != null)
688     {
689       sb.append("LINKED\n");
690       for (String id : linkedIds())
691       {
692         // already listed these as UNLINKED args
693         if (id == null)
694           continue;
695
696         ArgValuesMap avm = linkedArgs(id);
697         sb.append("ID: '").append(id).append("'\n");
698         sb.append(argValuesMapToString(avm));
699       }
700     }
701     return sb.toString();
702   }
703
704   private static String argValuesMapToString(ArgValuesMap avm)
705   {
706     if (avm == null)
707       return null;
708     StringBuilder sb = new StringBuilder();
709     for (Arg a : avm.getArgKeys())
710     {
711       ArgValues v = avm.getArgValues(a);
712       sb.append(v.toString());
713       sb.append("\n");
714     }
715     return sb.toString();
716   }
717
718   public static SubVals getSubVals(String item)
719   {
720     return new SubVals(item);
721   }
722
723   /**
724    * A helper class to keep an index of argument position with argument values
725    */
726   public static class ArgValue
727   {
728     private int argIndex;
729
730     private String value;
731
732     private String id;
733
734     protected ArgValue(String value, int argIndex)
735     {
736       this.value = value;
737       this.argIndex = argIndex;
738     }
739
740     protected String getValue()
741     {
742       return value;
743     }
744
745     protected int getArgIndex()
746     {
747       return argIndex;
748     }
749
750     protected void setId(String i)
751     {
752       id = i;
753     }
754
755     protected String getId()
756     {
757       return id;
758     }
759   }
760
761   /**
762    * A helper class to parse a string of the possible forms "content"
763    * "[index]content", "[keyName=keyValue]content" and return the integer index,
764    * the strings keyName and keyValue, and the content after the square brackets
765    * (if present). Values not set `will be -1 or null.
766    */
767   public static class SubVals
768   {
769     private static int NOTSET = -1;
770
771     private int index = NOTSET;
772
773     private Map<String, String> subVals = null;
774
775     private static char SEPARATOR = ';';
776
777     private String content = null;
778
779     public SubVals(String item)
780     {
781       this.parseVals(item);
782     }
783
784     public void parseVals(String item)
785     {
786       if (item.indexOf('[') == 0 && item.indexOf(']') > 1)
787       {
788         int openBracket = item.indexOf('[');
789         int closeBracket = item.indexOf(']');
790         String subvalsString = item.substring(openBracket + 1,
791                 closeBracket);
792         this.content = item.substring(closeBracket + 1);
793         boolean setIndex = false;
794         for (String subvalString : subvalsString
795                 .split(Character.toString(SEPARATOR)))
796         {
797           int equals = subvalString.indexOf('=');
798           if (equals > -1)
799           {
800             if (subVals == null)
801               subVals = new HashMap<>();
802             subVals.put(subvalString.substring(0, equals),
803                     subvalString.substring(equals + 1));
804           }
805           else
806           {
807             try
808             {
809               this.index = Integer.parseInt(subvalString);
810               setIndex = true;
811             } catch (NumberFormatException e)
812             {
813               Console.warn("Failed to obtain subvalue or index from '"
814                       + item + "'. Setting index=0 and using content='"
815                       + content + "'.");
816             }
817           }
818         }
819         if (!setIndex)
820           this.index = NOTSET;
821       }
822       else
823       {
824         this.content = item;
825       }
826     }
827
828     public boolean notSet()
829     {
830       // notSet is true if content present but nonsensical
831       return index == NOTSET && subVals == null;
832     }
833
834     public String get(String key)
835     {
836       return subVals == null ? null : subVals.get(key);
837     }
838
839     public boolean has(String key)
840     {
841       return subVals == null ? false : subVals.containsKey(key);
842     }
843
844     public int getIndex()
845     {
846       return index;
847     }
848
849     public String getContent()
850     {
851       return content;
852     }
853   }
854
855   /**
856    * Helper class to allow easy extraction of information about specific
857    * argument values (without having to check for null etc all the time)
858    */
859   protected static class ArgValuesMap
860   {
861     protected Map<Arg, ArgValues> m;
862
863     protected ArgValuesMap()
864     {
865       this.newMap();
866     }
867
868     protected ArgValuesMap(Map<Arg, ArgValues> map)
869     {
870       this.m = map;
871     }
872
873     private Map<Arg, ArgValues> getMap()
874     {
875       return m;
876     }
877
878     private void newMap()
879     {
880       m = new HashMap<Arg, ArgValues>();
881     }
882
883     private void newArg(Arg a)
884     {
885       if (m == null)
886         newMap();
887       if (!containsArg(a))
888         m.put(a, new ArgValues(a));
889     }
890
891     protected void addArgValue(Arg a, ArgValue av)
892     {
893       if (getMap() == null)
894         m = new HashMap<Arg, ArgValues>();
895
896       if (!m.containsKey(a))
897         m.put(a, new ArgValues(a));
898       ArgValues avs = m.get(a);
899       avs.addArgValue(av);
900     }
901
902     protected ArgValues getArgValues(Arg a)
903     {
904       return m == null ? null : m.get(a);
905     }
906
907     protected ArgValues getOrCreateArgValues(Arg a)
908     {
909       ArgValues avs = m.get(a);
910       if (avs == null)
911         newArg(a);
912       return getArgValues(a);
913     }
914
915     protected List<ArgValue> getArgValueList(Arg a)
916     {
917       ArgValues avs = getArgValues(a);
918       return avs == null ? new ArrayList<>() : avs.getArgValueList();
919     }
920
921     protected ArgValue getArgValue(Arg a)
922     {
923       List<ArgValue> vals = getArgValueList(a);
924       return (vals == null || vals.size() == 0) ? null : vals.get(0);
925     }
926
927     protected String getValue(Arg a)
928     {
929       ArgValue av = getArgValue(a);
930       return av == null ? null : av.getValue();
931     }
932
933     protected boolean containsArg(Arg a)
934     {
935       if (m == null || !m.containsKey(a))
936         return false;
937       return getArgValue(a) != null;
938     }
939
940     protected boolean hasValue(Arg a, String val)
941     {
942       if (m == null || !m.containsKey(a))
943         return false;
944       for (ArgValue av : getArgValueList(a))
945       {
946         String avVal = av.getValue();
947         if ((val == null && avVal == null)
948                 || (val != null && val.equals(avVal)))
949         {
950           return true;
951         }
952       }
953       return false;
954     }
955
956     protected boolean getBoolean(Arg a)
957     {
958       ArgValues av = getArgValues(a);
959       return av == null ? false : av.getBoolean();
960     }
961
962     protected Set<Arg> getArgKeys()
963     {
964       return m.keySet();
965     }
966
967     protected ArgValue getClosestPreviousArgValueOfArg(ArgValue thisAv,
968             Arg a)
969     {
970       ArgValue closestAv = null;
971       int thisArgIndex = thisAv.getArgIndex();
972       ArgValues compareAvs = this.getArgValues(a);
973       int closestPreviousIndex = -1;
974       for (ArgValue av : compareAvs.getArgValueList())
975       {
976         int argIndex = av.getArgIndex();
977         if (argIndex < thisArgIndex && argIndex > closestPreviousIndex)
978         {
979           closestPreviousIndex = argIndex;
980           closestAv = av;
981         }
982       }
983       return closestAv;
984     }
985
986     protected ArgValue getClosestNextArgValueOfArg(ArgValue thisAv, Arg a)
987     {
988       // this looks for the *next* arg that *might* be referring back to
989       // a thisAv. Such an arg would have no subValues (if it does it should
990       // specify an id in the subValues so wouldn't need to be guessed).
991       ArgValue closestAv = null;
992       int thisArgIndex = thisAv.getArgIndex();
993       ArgValues compareAvs = this.getArgValues(a);
994       int closestNextIndex = Integer.MAX_VALUE;
995       for (ArgValue av : compareAvs.getArgValueList())
996       {
997         int argIndex = av.getArgIndex();
998         if (argIndex > thisArgIndex && argIndex < closestNextIndex)
999         {
1000           closestNextIndex = argIndex;
1001           closestAv = av;
1002         }
1003       }
1004       return closestAv;
1005     }
1006
1007     protected ArgValue[] getArgValuesReferringTo(String key, String value,
1008             Arg a)
1009     {
1010       // this looks for the *next* arg that *might* be referring back to
1011       // a thisAv. Such an arg would have no subValues (if it does it should
1012       // specify an id in the subValues so wouldn't need to be guessed).
1013       List<ArgValue> avList = new ArrayList<>();
1014       Arg[] args = a == null ? (Arg[]) this.getMap().keySet().toArray()
1015               : new Arg[]
1016               { a };
1017       for (Arg keyArg : args)
1018       {
1019         for (ArgValue av : this.getArgValueList(keyArg))
1020         {
1021
1022         }
1023       }
1024       return (ArgValue[]) avList.toArray();
1025     }
1026
1027     protected boolean hasId(Arg a, String id)
1028     {
1029       ArgValues avs = this.getArgValues(a);
1030       return avs == null ? false : avs.hasId(id);
1031     }
1032
1033     protected ArgValue getId(Arg a, String id)
1034     {
1035       ArgValues avs = this.getArgValues(a);
1036       return avs == null ? null : avs.getId(id);
1037     }
1038   }
1039
1040   private static final Collection<Arg> bootstrapArgs = new ArrayList(
1041           Arrays.asList(Arg.PROPS, Arg.DEBUG));
1042
1043   public static Map<Arg, String> bootstrapArgs(String[] args)
1044   {
1045     Map<Arg, String> bootstrapArgMap = new HashMap<>();
1046     if (args == null)
1047       return bootstrapArgMap;
1048     Enumeration<String> argE = Collections.enumeration(Arrays.asList(args));
1049     while (argE.hasMoreElements())
1050     {
1051       String arg = argE.nextElement();
1052       String argName = null;
1053       String val = null;
1054       if (arg.startsWith("--"))
1055       {
1056         int equalPos = arg.indexOf('=');
1057         if (equalPos > -1)
1058         {
1059           argName = arg.substring(2, equalPos);
1060           val = arg.substring(equalPos + 1);
1061         }
1062         else
1063         {
1064           argName = arg.substring(2);
1065         }
1066         Arg a = argMap.get(argName);
1067         if (a != null && bootstrapArgs.contains(a))
1068           bootstrapArgMap.put(a, val);
1069       }
1070     }
1071     return bootstrapArgMap;
1072   }
1073 }