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