001// Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002// The Molly framework is freely distributable under the terms of an
003// MIT-style license. For details, see the molly pages web site at:
004// http://www.mollypages.org/. Use, modify, have fun !
005
006package fc.util;
007
008import java.io.*;
009import java.util.*;
010import java.text.*;
011
012import fc.io.*;
013
014/** 
015Manages arguments passed to an application.  Arguments can be optional or 
016required. Arguments received by this class have to be either of the
017following formats: 
018<blockquote>
019<tt>
020-flagname1 value 
021-flagname2=value2
022</tt>. 
023</blockquote>
024Arguments consist of flag name and flag value pairs. All flags must
025begin with a <tt>'-'</tt> character and any new word beginning
026with a <tt>'-'</tt> character is seen as the start of a new flag name
027(the <tt>'-'</tt> character is always permissible <b>inside</b> a
028token, i.e., after the first character). Additionaly, a flag name is
029delimited from it's corresponding value by either a space or the
030<tt>'='</tt> character. 
031<p>Note, in the above example, <tt>- flagname1</tt> is separated from
032it's value by a space whereas <tt>- flagname2</tt> is seperated from
033it's value by a <tt>'='</tt> character. Both delimiters are valid and
034can be intermixed on the same line.
035</p><p>
036Note also that <b>if the delimiter is a space, then the value part cannot
037begin with the <tt>-</tt> character</b>, but if the delimiter is <tt>=</tt>,
038then the value part can begin with the <tt>-</tt> character. That is, the
039following input is <b>valid</b>:
040<blockquote><tt>-flagname1=-value-for-flag1</tt></blockquote>
041<p>whereas this is <b>invalid</b>:
042<blockquote><tt>-flagname1 -value-for-flag1</tt></blockquote>
043Here, <tt>-value-for-flag1</tt> is seen as a new flag itself.
044</p><p>
045Following a flag name, all following values are assigned to that flagname,
046up until another flagname is encountered or the end of input is reached.
047Therefore, spaces do not have to be quoted. The following example,
048<blockquote>
049<tt>-path \some file\some where else -port 80</tt>
050</blockquote>
051is parsed as:
052<dl>
053<dt><tt><b>path</b></tt></dt>
054  <dd><tt>\some file\some where else</tt></dd>
055<dt><tt><b>port</b></tt></dt>
056  <dd><tt>80</tt></dd>
057</dl>
058</p><p>
059Different operating systems (and their command interpreters) also
060transform/modify command lines in various ways. This class (and
061Java/JVM) can only parse arguments that it recieves from the operating
062system and those may be different than what you may have specified on
063the command line, due to the aforementioned transformation. The
064following example,
065<blockquote>
066<tt>-path "/foo/bar /baz" -anotherpath \"a/b c/\"</tt>
067</blockquote>
068is parsed as:
069<dl>
070<dt><tt><b>path</b></tt></dt>
071  <dd><tt>/foo/bar /baz</tt></dd>
072<dt><tt><b>anotherpath</b></tt></dt>
073  <dd><tt>&quot;a/b c/&quot;</tt></dd>
074</dl>
075Note, that the <tt>path</tt> value has lost it's double quotes whereas
076the <tt>anotherpath</tt> value has not. This is because the O.S., in this case,
077stripped the quotes for <tt>path</tt> since they were not escaped on the
078command line. Note also that various unix'en convert command line args into 
079space delimited tokens. <code>bash</code> for example tokenizes the lines 
080using the IFS variable, and then feeds the tokens (separated by space) to the
081invoked program, in this case, the java interpreter.
082</p><p>
083Sometimes flags are specified <b>without</b> any corresponding value, with the
084existence of the flag itself implying some condition. However for clarity,
085it's sometimes better to explicitly set an option to yes or no. Therefore
086instead of saying <tt>-flag</tt>, always say <tt>-flag=[yes|no]</tt>, with 
087the value part being "yes" or "no". However, the existence of the flag by 
088itself <b>can</b> be simply checked by using the <code>flagExists()</code> method. 
089</p><p>
090Typical usage of this class may look like:
091<xmp>
092//inside the main method
093Args myargs = new Args(args);
094args.setUsage("java myApp -port 80 -foo=moo -debugport=5432"); //to inform user
095int port  = args.getRequired("port"); //get required "-port" flag
096String abc  = args.get("foo");  //get optional "-foo" flag
097String bar  = args.get("bar","5000"); //get optional "bar" flag, default to "5000"
098</xmp>
099</p>
100
101Thread Safety: This class <b>is</b> thread safe and can be used by multiple
102threads after it's instantiated.
103
104@author hursh jain  
105@version 1.0 7/15/2001
106**/
107public class Args
108{
109static boolean _internal_testing = false;
110
111protected   String    usage;
112protected   String[]  input;
113protected Map     flagMap;
114
115/** 
116If a flag name is not well formed, then the corresponding value (if any)
117is stored under this name. For example: <blockquote><tt>- abc</tt></blockquote>
118is missing a flagname (<tt>'-'</tt> followed by whitespace), therefore the 
119value <tt>abc</tt> is stored under this default name.
120**/
121public static String DEFAULT_FLAGNAME = "default";
122
123/** 
124If a flag name is repeated, then all corresponding values for that name are 
125concatenated with each other and the concatenated values are <b>delimited</b> 
126by this value. It's value in this class is <tt>,</tt>. For example: 
127<blockquote><tt>-foo abc -foo xyz</tt></blockquote> has the same flag repeated 
128twice (<tt>foo</tt>) and the resulting value for this flag will be <blockquote>
129<tt>abc<b>,</b>xyz</tt></blockquote>
130**/
131public static String FLAG_REPEAT_VALUE_DELIM = ",";
132
133/** 
134Creates a new instance, with the specified String[] to read arguments from. 
135Typically, the <tt>static void main(String[] args)</tt> method gets a String[]
136array from the JVM, containing the arguments passed to the application. This 
137array will typically be be passed to this constructor.
138**/
139public Args(String args[]) 
140  { 
141  input = args;
142  parseFlags();
143  }
144
145/** 
146Creates a new instance, with the specified InputStream to read arguments from.
147Reads as much as possible from the specified stream, so as to not block and
148delay the construction of this object. Any data not available via the specified
149InputStream at the time of construction, is not used later on and is essentially
150lost. The stream encoding is assumed to be ISO-8859-1, i.e., the flags names
151and values read from the stream are treated as normal ascii characters. Be aware
152of differences between parsing command line args and parsing args from a file, 
153because typically command line args are modified by the shell before being passed
154to the application.
155**/
156public Args(InputStream in) 
157  { 
158  toStringArray(in);  
159  parseFlags();
160  }
161
162
163/** 
164Checks if the specified flag exits, regardless of whether a corresponding
165value exist for that flag. For example, suppose the invoked arguments looked
166like: 
167<blockquote><tt>-foo -bar=baz</tt></blockquote>. Then the flag <tt>foo</tt> 
168would exist but have no corresponding value.
169@param  flagname the flagname to check
170@return true if flag exists, false otherwise
171**/
172public boolean flagExists(String flagname) 
173  {
174  boolean exists = false;
175  if (this.flagMap.containsKey(flagname)) 
176    {
177    exists = true;
178    }
179  return exists;
180  }
181
182/** Returns the total number of flags available**/
183public int getFlagCount() {
184  return flagMap.size();
185  }
186
187/** 
188Returns the fully qualified name of the class that contained the <tt>main()</tt> method used to
189invoke the application. <b>Note: </b> this method must be called from the same thread (the "main"
190thread) that started the application. Newly started threads that call this method will instead
191obtain the class name containing that thread's <tt>run()</tt> method.
192**/
193public String getMainClassName() 
194  {
195  return ClassUtil.getMainClassName();
196  }
197
198
199/** 
200Returns an argument by positional value. <tt>get(n)</tt>
201is the same as <tt>static void main(String[] args) --> args[n]
202</tt> and is provided here for convenience.
203
204@param  n   the 0-based index of the String[] passed to main()
205@throws NullPointerException 
206      if the specified index is greater than the number
207      of arguments
208**/
209public String get(int n) {
210  return input[n];
211  }
212
213/** 
214Returns a required argument by positional value. <tt>get(n)</tt>
215is the same as <tt>static void main(String[] args) --> args[n]
216</tt> and is provided here for convenience. Ff the specified 
217index is greater than the number of arguments, then the error
218is handled in the same way as {@link #getRequired(String)}
219
220@param  n   the 0-based index of the String[] passed to main()
221**/
222public String getRequired(int n) {
223  if (n < 0 || n > (input.length - 1)) {
224    handleError("Missing required argument at position [" + n + "]");
225    }
226  return input[n];
227  }
228
229
230/** 
231Returns the value corresponding to the specified flag. 
232
233@param  flagname  the name of the flag <b>without</b> the leading <tt>-</tt> character
234@return <tt>null</tt> either if the flag itself was not found, or if the 
235    flag was found but no associated value was found. 
236**/
237public String get(String flagname)
238  {
239  return (String) flagMap.get(flagname);  
240  }
241
242/** 
243Returns the value corresponding to the specified flag. If
244the flag and/or it's value is not found, this method returns
245the specified backup value instead.
246
247@param  flagname  the name of the flag <b>without</b> the leading <tt>-</tt> character
248@param  backup    the value to return if flag not found 
249@return value of specified flag or backup string
250**/
251public String get(String flagname, String backup)
252  {
253  String val = get(flagname);
254  if (val == null) 
255    {
256    return backup;  
257    }
258  else return val;
259  }
260
261
262/**
263Returns the value corresponding to the specified flag. If
264the flag and/or it's value is not found, this method returns
265the specified backup value instead.
266
267If the property <b>is</b> present, then the corresponding
268value is <tt>true</tt> if the property value is any of the
269following (case-insensitive):
270<blockquote>
271<code>
272yes, 1, true
273</code>
274<blockquote>
275else <tt>false</tt> is returned. Also see {@link #flagExists}.
276
277@param  flagname  the name of the flag <b>without</b>
278          the leading <tt>-</tt> character
279@param  backup    value to return if the property for the 
280          specified property is <b>not</b> present  
281
282@return value of specified key or backup string
283*/
284public boolean getBoolean(String flagname, boolean backup) 
285  {
286  String val = get(flagname);
287
288    if (val == null)
289       return backup;
290    else
291       return toBoolean(val);
292    }
293
294
295/**
296Returns the value corresponding to the specified flag. If the flag and/or it's value is not found
297or is found but cannot be converted into an integer (via a {@link Integer.parseInt(String)} call),
298the backup value will be returned.
299
300@param  flagname  the name of the flag <b>without</b> the leading <tt>-</tt> character
301@param  backup    value to return if the property for the specified property is <b>not</b>
302          present 
303
304@return value of specified key or backup string
305*/
306public int getInt(String flagname, int backup) 
307  {
308    String val = get(flagname);
309    if (val != null)
310      {
311      try {
312        return Integer.parseInt(val);
313        }
314      catch (NumberFormatException e) {
315        System.err.println("Cannot convert flag '" + flagname + "', into a number, returning backup value " + backup);
316        }
317      }
318  return backup;
319    }
320
321
322/**
323Returns the value corresponding to the specified flag. If the flag and/or it's value is not found
324or is found but cannot be converted into an long (via a {@link Long.parseLong(String)} call),
325the backup value will be returned.
326
327@param  flagname  the name of the flag <b>without</b> the leading <tt>-</tt> character
328@param  backup    value to return if the property for the specified property is <b>not</b>
329          present 
330
331@return value of specified key or backup string
332*/
333public long getLong(String flagname, long backup) 
334  {
335    String val = get(flagname);
336    if (val != null)
337      {
338      try {
339        return Long.parseLong(val);
340        }
341      catch (NumberFormatException e) {
342        System.err.println("Cannot convert flag '" + flagname + "', into a number, returning backup value " + backup);
343        }
344      }
345  return backup;
346    }
347
348
349/** 
350Returns the value corresponding to the specified flag. If the flag's value is not found (even if
351the flag name exists), this method calls the <code>error method</code>, which by default prints a
352stack trace and exits the application.
353
354<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will
355return <tt>null</tt> if the specified flag is not found.
356
357@param  flagname  the name of the flag <b>without</b> the leading <tt>'-'</tt> character
358@return value of specified flag
359@see  #handleError
360**/
361public String getRequired(String flagname)
362  {
363  if (flagname.equals("")) {
364    handleError("This program expects invocation flag(s).");
365    return null;
366    }
367    
368  String val = get(flagname);
369  if (val == null) {
370    handleError("Missing required flag: " + flagname);
371    }
372  
373  return val;
374  }
375
376/** another alias for {@link getRequired(String)} */
377public String getRequiredString(String flagname)
378  {
379  return getRequired(flagname);
380  }
381
382/*
383Returns the value obtained via {@link getRequired(String)} as a boolean. The boolean value
384returned is <code>true</code> if the property value is any of the following (case-insensitive):
385<blockquote>
386<code>
387yes, 1, true
388</code>
389<blockquote>
390else <tt>false</tt> is returned.
391
392<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will
393return <tt>false</tt> if the specified flag is not found.
394
395@param  flagname  the name of the flag <b>without</b> the leading <tt>'-'</tt> character
396@return value of specified flag as a boolean
397@see  #handleError
398*/
399public boolean getRequiredBoolean(String flagname) 
400  {
401  String val = getRequired(flagname);
402  return toBoolean(val);  
403  }
404
405
406/**
407Returns the value obtained via {@link getRequired(String)} as a File.
408
409<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will
410return <tt>false</tt> if the specified file does not exist.
411*/
412public File getRequiredFile(String flagname) 
413  {
414  String val = getRequired(flagname);
415  File f = new File(val);
416  if (! f.exists()) {
417    handleError(flagname + " does not exist/is not a valid file");
418    }
419  return f;
420  }
421
422/*
423Returns the value obtained via {@link getRequired(String)} as as an integer. If the property cannot
424be converted into an integer (via a {@link Integer.parseInt(String)} call), the resulting action
425would be the same as if the required flag was not present.
426
427<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will
428return <tt>0</tt> if the specified flag is not found.
429
430@param  flagname  the name of the flag <b>without</b> the leading <tt>'-'</tt> character
431@return value of specified flag as a int
432@see  #handleError
433*/
434public int getRequiredInt(String flagname)
435  {
436  String val = getRequired(flagname);
437    
438    int result = 0;
439    
440    try {
441      result = Integer.parseInt(val);
442      }
443  catch (NumberFormatException ne) {
444      System.err.println("Cannot convert flag '" + flagname + "', into a number");
445    handleError(flagname);
446    }
447  
448  return result;
449  }
450
451  
452/** another alias for {@link getRequiredInt(String)} */
453public int getRequiredInteger(String flagname)
454  {
455  return getRequiredInt(flagname);
456  }
457  
458
459/*
460Returns the value obtained via {@link getRequired(String)} as as an long. If the property cannot
461be converted into an long (via a {@link Long.parseLong(String)} call), the resulting action
462would be the same as if the required flag was not present.
463
464<p>If the handleError method is overriden to <b>not</b> exit the application, then this method will
465return <tt>0</tt> if the specified flag is not found.
466
467@param  flagname  the name of the flag <b>without</b> the leading <tt>'-'</tt> character
468@return value of specified flag as a long
469@see  #handleError
470*/
471public long getRequiredLong(String flagname)
472  {
473  String val = getRequired(flagname);
474    
475    long result = 0;
476    
477    try {
478      result = Long.parseLong(val);
479      }
480  catch (NumberFormatException ne) {
481      System.err.println("Cannot convert flag '" + flagname + "', into a number");
482    handleError(flagname);
483    }
484  
485  return result;
486  }
487
488  
489/** 
490Returns the value as a Date. Understands the following (case-insensitive) values
491<dl>
492<dt><tt>now</tt>, <tt>today</tt></dt>
493  <dd>today's date (this instant)</dd>
494<dt><tt>tomorrow</tt></dt>
495  <dd>tomorrow's date (24 hours from now)</dd>
496<dt><tt>nextweek</tt></dt>
497  <dd>a date representing 1 week from now</dd>
498<dt><tt>atweekbegin</tt>, <tt>atbeginweek</tt>, <tt>atbeginofweek</tt></dt>
499  <dd>first day of the current week</dd>
500</dl>
501<p>
502If none of these special keywords are found, returns the value by trying to parsing 
503it as: <tt>yyyy-MM-dd HH:mm:ss</tt> or <tt>yyyy-MM-dd</tt>
504</p><p>
505If the date cannot be parsed using either of the above patterns, an exception is 
506thrown.
507*/
508public Date getRequiredFriendlyDate(String flagname)
509  {
510  Calendar cal = Calendar.getInstance(Locale.US);
511    
512  java.util.Date date = null;
513    
514  String flagval = getRequired(flagname);
515  flagval = flagval.toLowerCase().intern();
516    
517  if (flagval == "now" || flagval == "today") {
518    date = new java.util.Date();
519    }
520  else if (flagval == "tomorrow") {
521    date = new java.util.Date(System.currentTimeMillis() + CalendarUtil.TWENTY_FOUR_HOUR);
522    }
523  else if (flagval == "nextweek") {
524    date = new java.util.Date(System.currentTimeMillis() + CalendarUtil.ONE_WEEK);
525    }
526  else if (flagval == "atweekbegin" || flagval == "atbeginweek" 
527      || flagval == "atbeginofweek") {
528    date = CalendarUtil.getFirstDayOfWeek(cal, new Date());
529    }
530  else{ 
531    try {
532      date = parseDate(flagval);
533      }
534    catch (ParseException e) {
535      handleError("Cannot parse flag name=["+ flagname + "], value=[" + flagval + "] as a friendly date..");
536      }
537    }
538    
539  return date;
540  }
541  
542
543/** 
544Returns the value as a Date. Understands the following (case-insensitive) values
545<dl>
546<dt><tt>now</tt>, <tt>today</tt></dt>
547  <dd>today's date (this instant)</dd>
548<dt><tt>tomorrow</tt></dt>
549  <dd>tomorrow's date (24 hours from now)</dd>
550<dt><tt>nextweek</tt></dt>
551  <dd>a date representing 1 week from now</dd>
552<dt><tt>atweekbegin</tt>, <tt>atbeginweek</tt>, <tt>atbeginofweek</tt></dt>
553  <dd>first day of the current week</dd>
554</dl>
555<p>
556If none of these special keywords are found, returns the value by trying to parsing 
557it as: <tt>yyyy-MM-dd HH:mm:ss</tt> or <tt>yyyy-MM-dd</tt>
558</p><p>
559The flag is optional but if it is present and cannot be parsed using either of the above patterns, an exception is 
560thrown.
561*/
562public Date getFriendlyDate(String flagname)
563  {
564  Calendar cal = Calendar.getInstance(Locale.US);
565    
566  java.util.Date date = null;
567    
568  String flagval = get(flagname);
569  if (flagval == null) {
570    return null;
571    }
572  
573  return getRequiredFriendlyDate(flagname);
574  }
575  
576  
577/**
578Checks to see that one and only 1 of the specified flags is present. Useful for mutualy exclusive arguments.
579*/
580public void onlyOneOf(String... flags)
581  {
582  if (flags == null) {
583    throw new IllegalArgumentException("Specify at least one argument to this method!");
584    }
585  
586  BitSet b = new BitSet(flags.length);
587  
588  for (int n = 0; n < flags.length; n++) {
589    if (flagExists(flags[n])) {
590      b.set(n);
591      }
592    }
593    
594  if (! (b.cardinality() == 1)) {
595    handleError(
596      b.cardinality() + " mutually exclusive flags specified. Can only specify one of the following: \n    " + Arrays.toString(flags) + "\n");
597    }
598  }
599
600/**
601Checks to see that one and only 1 of the specified flags is present. Useful for mutualy exclusive arguments.
602Alias for the {@link onlyOneOf} method.
603*/
604public void isMutuallyExclusive(String... flags)
605  {
606  onlyOneOf(flags); 
607  }
608
609  
610private static java.util.Date parseDate(String s) throws ParseException
611  {
612  SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
613  SimpleDateFormat df2 = new SimpleDateFormat("yyyy-MM-dd");
614    
615  java.util.Date date = null;
616  try { 
617    date = df.parse(s);
618    }
619  catch (ParseException e) {
620    date = df2.parse(s);
621    }
622
623  return date;
624  } 
625
626
627/**
628Returns the raw String[] passed to this object during construction. If
629an InputStream was passed in via construction, returns a String[] created
630from that InputStream (tokenized by whitespace). The returned String[]
631is modifiable but it's modification will not affect any other method in
632this class.
633@return String[] of unparsed values
634**/
635public String[] rawValues() 
636  {
637  return this.input;
638  }
639
640
641/**
642Returns a Map containing all flagname/flagvalue pairs. The returned
643map is an <b>unmodifiable</b> view of the map contained in this class.
644@return Map of flagnames/values
645**/
646public Map values()
647  {
648  return Collections.unmodifiableMap(this.flagMap);
649  }
650
651
652/** 
653Specify program usage information to be output when an error occurs. This 
654information should contain a short description of expected and/or optional
655flags and information that the application expects.
656@param str  Usage information
657**/
658public void setUsage(String str)
659  {
660  this.usage = str;   
661  }
662
663/** 
664Convenience method to display the usage information in case
665of program error. Reads and sets the usage file found in the
666same directory/resource location as the calling object's
667package. For example, if clazz <tt>a.b.foo</tt> calls this
668method, then the usage file should exist in the <tt>a/b</tt>
669directory (typically starting from the classpath or a
670jar/zip file).
671<p>
672The name of the usage file is the same as the class name of
673the specified object (without the ending ".java") appended
674with "<tt>_usage.txt</tt>". Therefore the usage file for
675code residing in <tt>a.b.foo.java</tt> should be called
676<tt>foo_usage.txt</tt> and reside in the <tt>a/b</tt>
677directory.
678
679@param  caller  the calling object <i>or</i> {@link java.lang.Class} Class 
680        corresponding to that object
681**/
682public void setDefaultUsage(Object caller)
683  {
684  try {
685    Class clazz = caller.getClass();
686    
687    String usageFile = StringUtil.fileName(clazz.getName().replace('.', '/')) + "_usage.txt";
688    
689    InputStream in = clazz.getResourceAsStream(usageFile);
690      if (in != null) 
691        setUsage( IOUtil.inputStreamToString(in, false) );
692      else 
693        setUsage( "Cannot display usage instructions, " + usageFile + " not found");
694      }
695  catch (IOException e) {
696    e.printStackTrace();
697    }
698  }
699
700/** 
701Convenience method to display the usage information in case of
702program error. Reads and sets the usage file found specified by
703the <tt>pathToFile</tt> argument of this method. This path should
704be a path to a file reachable from any directory in the classpath
705(i.e., relative to the classpath) and should not start with a "/"
706**/
707public void setDefaultUsage(String pathToFile)
708  {
709  try {
710    InputStream in = ClassLoader.getSystemResourceAsStream(pathToFile);
711    if (in != null) 
712      setUsage( IOUtil.inputStreamToString(in, false) );
713    else 
714      setUsage( "Cannot display usage instructions, " + pathToFile + " not found");
715    }
716  catch (IOException e) {
717    e.printStackTrace();
718    }
719  }
720
721/** 
722Invoking this method gives the same results as invoking
723{@link #getRequired(String) getRequired} with a non-existent flag name.
724**/
725public void showError() {
726  getRequired("");
727  }
728
729/** 
730Returns the PrintStream where error messages will be sent. This
731method can be overriden in sublasses to return a different PrintStream. 
732**/
733protected PrintStream getOutputStream()
734  {
735  return System.err;
736  }
737
738final boolean toBoolean(String val)
739  {
740  val = val.trim().toLowerCase().intern();
741  if ( val == "true" || val == "yes" || val == "1" )
742    return true;
743  return false;
744  }
745
746/** 
747Handles an error situation such as when a required flag is
748not found. By default, calls the <tt>getOutputStream</tt> method,
749prints an error message and exits the JVM by calling <tt>System.exit(1)</tt>
750This method can be overriden in subclasses to handle errors differently.
751@param  str   the error message
752@see #getOutputStream
753**/
754protected void handleError(String str)
755  {
756  PrintStream out = getOutputStream();
757  out.println(str);
758  if (usage != null) {
759    out.println(usage);
760    }
761  out.println();
762  out.println("-------- Stack Trace --------");
763  new Exception().printStackTrace(out);
764  if (! _internal_testing) {
765    System.exit(1);
766    }
767  }
768
769public static void main(String[] args) throws Exception
770  {
771  new Test(args);   
772  }
773
774static private class Test
775{
776Test(String[] args) throws Exception
777  {
778  Args myarg = new Args(args);
779  System.out.println("Main Class Name: " + myarg.getMainClassName());
780  System.out.println("Command line parsed as: "+myarg.values());  
781  
782  String testbytes =  
783    "-port 80 -port2=5000 -flow c:\\program files -foo " +  
784    "-moo \"moo moo\" -x aaa -x bbb - default fasf";
785  
786  Args myarg2 = new Args(new ByteArrayInputStream(testbytes.getBytes("ISO-8859-1")));
787  Map values = myarg2.values();
788  System.out.println("testbyte string parsed as: " + values); 
789  try { 
790    values.put("foo","bar"); 
791    } 
792  catch (Exception e) { e.printStackTrace(); }
793  String[] rawvals = myarg2.rawValues();
794  System.out.println("raw values="+Arrays.asList(rawvals));
795  System.out.println("get(port2)="+myarg2.get("port2"));
796  System.out.println("get(default)="+myarg2.get(DEFAULT_FLAGNAME));
797  System.out.println("get(x)="+myarg2.get("x"));
798  System.out.println("flagExists(moo)="+myarg2.flagExists("flow")); 
799  System.out.println("flagExists(moo)="+myarg2.flagExists("foo")); 
800  System.out.println("flagExists(doesnotexits)="+myarg2.flagExists("doesnotexist")); 
801
802  testbytes = "-testdate now -testdate2 nextweek -testdate3 atbeginweek -testdate4 2015-03-01";
803  Args myarg3 = new Args(new ByteArrayInputStream(testbytes.getBytes("ISO-8859-1")));
804  System.out.println("friendly date test, using as input:\n\t" + testbytes);
805  System.out.println("now="+new Date());
806  System.out.println("testdate1="+myarg3.getRequiredFriendlyDate("testdate")); 
807  System.out.println("testdate2="+myarg3.getRequiredFriendlyDate("testdate2")); 
808  System.out.println("testdate3="+myarg3.getRequiredFriendlyDate("testdate3")); 
809  System.out.println("testdate4="+myarg3.getRequiredFriendlyDate("testdate4"));
810
811  //prevents system.exit being called.
812  _internal_testing = true;
813  
814  try {
815    myarg2.setUsage("Usage: java Args.java your arguments here");
816    myarg2.getRequired("doesnotexist");
817    }
818  catch (Exception e) {
819    System.out.println(IOUtil.throwableToString(e));
820    }
821    
822  try {
823    testbytes = "-flag1 -flag2 -flag3";
824    Args myarg4 = new Args(new ByteArrayInputStream(testbytes.getBytes("ISO-8859-1")));
825
826    myarg4.setUsage("Testing only 1 mutually flag present");
827    System.out.print("this should work...myarg4.onlyOneOf(\"flag1\")");
828    myarg4.onlyOneOf("flag1");
829    System.out.println("..yes");
830
831    System.out.println("this should fail..");
832    myarg4.onlyOneOf("flag1", "flag2");
833    }
834  catch (Exception e) {
835    System.out.println(IOUtil.throwableToString(e));
836    }
837  }
838} //~class Test 
839  
840//-------------------- implementation helper methods ----------------------
841
842//converts the inputstream into a whitespace delimited array of tokens
843//and then saves those tokens as the input (a String[]) instance variable
844private void toStringArray(InputStream in)
845  {
846  try {
847    int len = in.available();
848    byte[] arr = new byte[len]; 
849    //will read upto len bytes into arr, shouldn't block
850    new BufferedInputStream(in).read(arr, 0, len);  
851      String temp = new String(arr, "ISO-8859-1");
852    //default tokenizer delimits by all whitespace and does 
853    //not return delimiters themselves.
854      StringTokenizer st = new StringTokenizer(temp); 
855      int toknum = st.countTokens();
856      this.input = new String[toknum];
857      int n = 0;
858      while (st.hasMoreTokens()) {
859        this.input[n++] = st.nextToken().trim();
860        }
861      }
862  catch (IOException e) {
863    this.input = null;
864    }
865  }
866
867//reads this.input(String[]), parses and stores the arguments in this.flagMap (as
868//key=value pairs). A state machine approach to this is *really* painful, surprisingly 
869//so. (i tried it and it was 5 times the lines of code over this method and it was still
870//kinda buggy), while this method, although apparently hacky, works like a charm.
871private void parseFlags()
872  {
873  if (input == null) {
874    return;
875    }
876  flagMap = new HashMap();
877  String flagname = null, flagvalue = null;
878  int fp = findNextFlag(0);
879  int nextfp = 0;
880  int endvalp = 0; //current flagvalue to be read until this position
881  while (fp != -1) 
882    {
883    flagname = input[fp];
884    flagvalue = null;
885    int eqindex = flagname.indexOf('=');
886    if (eqindex != -1)  {
887      flagvalue = flagname.substring(eqindex+1, flagname.length());
888      flagname = flagname.substring(0, eqindex);
889      } 
890    nextfp = findNextFlag(fp+1); 
891    endvalp = (nextfp == -1) ? input.length : nextfp;
892    for (int n = fp + 1; n < endvalp; n++) {
893      if (flagvalue == null ) { flagvalue = input[n]; }
894      else          { flagvalue += " " + input[n]; }
895      }
896    if (flagname.length() == 1) { 
897      flagname = DEFAULT_FLAGNAME;  
898      } 
899    else { 
900      flagname = flagname.substring(1,flagname.length()); 
901      }
902    if (flagMap.containsKey(flagname)) { //append if flagvalue already exists
903      flagvalue = flagMap.get(flagname) + FLAG_REPEAT_VALUE_DELIM + flagvalue;
904      }
905    flagMap.put(flagname, flagvalue);
906    fp = nextfp;
907//    for debugging
908//    System.out.println("flagname="+flagname);
909//    System.out.println(eqindex);
910//    System.out.println("fp, nextfp, endvalp = "+fp+","+nextfp+","+endvalp);
911//    System.out.println("flagname, flagvalue="+flagname +"," + flagvalue);
912    }   //~while     
913  }   //~parseFlags
914
915//finds the position of the next -flag starting from pos or -1 if no flag found
916private int findNextFlag(int pos) 
917  {
918  for (int n = pos; n < input.length; n++) 
919    {
920//    System.out.println("n, input[n]="+n+","+input[n]);
921    if (input[n].startsWith("-")) 
922      {
923      return n;
924      }
925    }
926  return -1;
927  }
928
929}   //~class Args