public class CommandLine {
    
    /** the unrecognised options/arguments */
    private List args    = new LinkedList();

    /** the processed options */
    private Map options = new HashMap();

    /** Map of unique options for ease to get complete list of options */
    private Map hashcodeMap = new HashMap();

    /** the processed options */
    private Option[] optionsArray;

    /**
     * <p>Creates a command line.</p>
     */
    CommandLine() {
    }
    
    /** 
     * <p>Query to see if an option has been set.</p>
     *
     * @param opt Short name of the option
     * @return true if set, false if not
     */
    public boolean hasOption(String opt) {
        return options.containsKey( opt );
    }

    /** 
     * <p>Query to see if an option has been set.</p>
     *
     * @param opt character name of the option
     * @return true if set, false if not
     */
    public boolean hasOption( char opt ) {
        return hasOption( String.valueOf( opt ) );
    }

    /**
     * <p>Return the <code>Object</code> type of this <code>Option</code>.</p>
     *
     * @param opt the name of the option
     * @return the type of this <code>Option</code>
     */
    public Object getOptionObject( String opt ) {
        String res = getOptionValue( opt );
        
        Object type = ((Option)((List)options.get(opt)).iterator().next()).getType();
        return res == null ? null : TypeHandler.createValue(res, type);
    }

    /**
     * <p>Return the <code>Object</code> type of this <code>Option</code>.</p>
     *
     * @param opt the name of the option
     * @return the type of opt
     */
    public Object getOptionObject( char opt ) {
        return getOptionObject( String.valueOf( opt ) );
    }

    /** 
     * <p>Retrieve the argument, if any, of this option.</p>
     *
     * @param opt the name of the option
     * @return Value of the argument if option is set, and has an argument,
     * otherwise null.
     */
    public String getOptionValue( String opt ) {
        String[] values = getOptionValues(opt);
        return (values == null) ? null : values[0];
    }

    /** 
     * <p>Retrieve the argument, if any, of this option.</p>
     *
     * @param opt the character name of the option
     * @return Value of the argument if option is set, and has an argument,
     * otherwise null.
     */
    public String getOptionValue( char opt ) {
        return getOptionValue( String.valueOf( opt ) );
    }

    /** 
     * <p>Retrieves the array of values, if any, of an option.</p>
     *
     * @param opt string name of the option
     * @return Values of the argument if option is set, and has an argument,
     * otherwise null.
     */
    public String[] getOptionValues( String opt ) {
        List values = new java.util.ArrayList();

        if( options.containsKey( opt ) ) {
            List opts = (List)options.get( opt );
            Iterator iter = opts.iterator();

            while( iter.hasNext() ) {
                Option optt = (Option)iter.next();
                values.addAll( optt.getValuesList() );
            }
        }
        return (values.size() == 0) ? null : (String[])values.toArray(new String[]{});
    }

    /** 
     * <p>Retrieves the array of values, if any, of an option.</p>
     *
     * @param opt character name of the option
     * @return Values of the argument if option is set, and has an argument,
     * otherwise null.
     */
    public String[] getOptionValues( char opt ) {
        return getOptionValues( String.valueOf( opt ) );
    }
    
    /** 
     * <p>Retrieve the argument, if any, of an option.</p>
     *
     * @param opt name of the option
     * @param defaultValue is the default value to be returned if the option is not specified
     * @return Value of the argument if option is set, and has an argument,
     * otherwise <code>defaultValue</code>.
     */
    public String getOptionValue( String opt, String defaultValue ) {
        String answer = getOptionValue( opt );
        return ( answer != null ) ? answer : defaultValue;
    }
    
    /** 
     * <p>Retrieve the argument, if any, of an option.</p>
     *
     * @param opt character name of the option
     * @param defaultValue is the default value to be returned if the option is not specified
     * @return Value of the argument if option is set, and has an argument,
     * otherwise <code>defaultValue</code>.
     */
    public String getOptionValue( char opt, String defaultValue ) {
        return getOptionValue( String.valueOf( opt ), defaultValue );
    }

    /** 
     * <p>Retrieve any left-over non-recognized options and arguments</p>
     *
     * @return remaining items passed in but not parsed as an array
     */
    public String[] getArgs() {
        String[] answer = new String[ args.size() ];
        args.toArray( answer );
        return answer;
    }
    
    /** 
     * <p>Retrieve any left-over non-recognized options and arguments</p>
     *
     * @return remaining items passed in but not parsed as a <code>List</code>.
     */
    public List getArgList() {
        return args;
    }
    
    /** 
     * jkeyes
     * - commented out until it is implemented properly
     * <p>Dump state, suitable for debugging.</p>
     *
     * @return Stringified form of this object
     */
    /*
    public String toString() {
        StringBuffer buf = new StringBuffer();
        
        buf.append( "[ CommandLine: [ options: " );
        buf.append( options.toString() );
        buf.append( " ] [ args: ");
        buf.append( args.toString() );
        buf.append( " ] ]" );
        
        return buf.toString();
    }
    */

    /**
     * <p>Add left-over unrecognized option/argument.</p>
     *
     * @param arg the unrecognised option/argument.
     */
    void addArg(String arg) {
        args.add( arg );
    }
        
    /**
     * <p>Add an option to the command line.  The values of 
     * the option are stored.</p>
     *
     * @param opt the processed option
     */
    void addOption( Option opt ) {
        hashcodeMap.put( new Integer( opt.hashCode() ), opt );

        String key = opt.getOpt();
        if( " ".equals(key) ) {
            key = opt.getLongOpt();
        }

        if( options.get( key ) != null ) {
            ((java.util.List)options.get( key )).add( opt );
        }
        else {
            options.put( key, new java.util.ArrayList() );
            ((java.util.List)options.get( key ) ).add( opt );
        }
    }

    /**
     * <p>Returns an iterator over the Option members of CommandLine.</p>
     *
     * @return an <code>Iterator</code> over the processed {@link Option} 
     * members of this {@link CommandLine}
     */
    public Iterator iterator( ) {
        return hashcodeMap.values().iterator();
    }

    /**
     * <p>Returns an array of the processed {@link Option}s.</p>
     *
     * @return an array of the processed {@link Option}s.
     */
    public Option[] getOptions( ) {
        Collection processed = hashcodeMap.values();

        // reinitialise array
        optionsArray = new Option[ processed.size() ];

        // return the array
        return (Option[]) processed.toArray( optionsArray );
    }

} public interface CommandLineParser {
    
    /**
     * Parse the arguments according to the specified options.
     *
     * @param options the specified Options
     * @param arguments the command line arguments
     * @return the list of atomic option and value tokens
     * @throws ParseException if there are any problems encountered
     * while parsing the command line tokens.
     */
    public CommandLine parse( Options options, String[] arguments )
    throws ParseException;

    /**
     * Parse the arguments according to the specified options.
     *
     * @param options the specified Options
     * @param arguments the command line arguments
     * @param stopAtNonOption specifies whether to continue parsing the
     * arguments if a non option is encountered.
     * @return the list of atomic option and value tokens
     * @throws ParseException if there are any problems encountered
     * while parsing the command line tokens.
     */
    public CommandLine parse( Options options, String[] arguments, boolean stopAtNonOption )
    throws ParseException;
} public class GnuParser extends Parser {

    /** holder for flattened tokens */
    private ArrayList tokens = new ArrayList();

    /**
     * <p>Resets the members to their original state i.e. remove
     * all of <code>tokens</code> entries.
     */
    private void init() {
        tokens.clear();
    }

    /**
     * <p>This flatten method does so using the following rules:
     * <ol>
     *  <li>If an {@link Option} exists for the first character of 
     *  the <code>arguments</code> entry <b>AND</b> an {@link Option} 
     *  does not exist for the whole <code>argument</code> then
     *  add the first character as an option to the processed tokens
     *  list e.g. "-D" and add the rest of the entry to the also.</li>
     *  <li>Otherwise just add the token to the processed tokens list.
     *  </li>
     * </ol>
     * </p>
     */
    protected String[] flatten( Options options, 
                                String[] arguments, 
                                boolean stopAtNonOption )
    {
        init();
        boolean eatTheRest = false;
        Option currentOption = null;

        for( int i = 0; i < arguments.length; i++ ) {
            if( "--".equals( arguments[i] ) ) {
                eatTheRest = true;
                tokens.add( "--" );
            }
            else if ( "-".equals( arguments[i] ) ) {
                tokens.add( "-" );
            }
            else if( arguments[i].startsWith( "-" ) ) {
                Option option = options.getOption( arguments[i] );

                // this is not an Option
                if( option == null ) {
                    // handle special properties Option
                    Option specialOption = options.getOption( arguments[i].substring(0,2) );
                    if( specialOption != null ) {
                        tokens.add( arguments[i].substring(0,2) );
                        tokens.add( arguments[i].substring(2) );
                    }
                    else if( stopAtNonOption ) {
                        eatTheRest = true;
                        tokens.add( arguments[i] );
                    }
                    else {
                        tokens.add( arguments[i] );
                    }
                }
                else {
                    currentOption = option;
                    // special option
                    Option specialOption = options.getOption( arguments[i].substring(0,2) );
                    if( specialOption != null && option == null ) {
                        tokens.add( arguments[i].substring(0,2) );
                        tokens.add( arguments[i].substring(2) );
                    }
                    else if( currentOption != null && currentOption.hasArg() ) {
                        if( currentOption.hasArg() ) {
                            tokens.add( arguments[i] );
                            currentOption= null;
                        }
                        else if ( currentOption.hasArgs() ) {
                            tokens.add( arguments[i] );
                        }
                        else if ( stopAtNonOption ) {
                            eatTheRest = true;
                            tokens.add( "--" );
                            tokens.add( arguments[i] );
                        }
                        else {
                            tokens.add( arguments[i] );
                        }
                    } 
                    else if (currentOption != null ) {
                        tokens.add( arguments[i] );
                    } 
                    else if ( stopAtNonOption ) {
                        eatTheRest = true;
                        tokens.add( "--" );
                        tokens.add( arguments[i] );
                    }
                    else {
                        tokens.add( arguments[i] );
                    }
                }
            }
            else {
                tokens.add( arguments[i] );
            }

            if( eatTheRest ) {
                for( i++; i < arguments.length; i++ ) {
                    tokens.add( arguments[i] );
                }
            }
        }
        return (String[])tokens.toArray( new String[] {} );
    }
} public class HelpFormatter
{
   // --------------------------------------------------------------- Constants

   public static final int DEFAULT_WIDTH              = 74;
   public static final int DEFAULT_LEFT_PAD           = 1;
   public static final int DEFAULT_DESC_PAD           = 3;
   public static final String DEFAULT_SYNTAX_PREFIX   = "usage: ";
   public static final String DEFAULT_OPT_PREFIX      = "-";
   public static final String DEFAULT_LONG_OPT_PREFIX = "--";
   public static final String DEFAULT_ARG_NAME        = "arg";

   // ------------------------------------------------------------------ Static

   // -------------------------------------------------------------- Attributes

   public int defaultWidth;
   public int defaultLeftPad;
   public int defaultDescPad;
   public String defaultSyntaxPrefix;
   public String defaultNewLine;
   public String defaultOptPrefix;
   public String defaultLongOptPrefix;
   public String defaultArgName;

   // ------------------------------------------------------------ Constructors
   public HelpFormatter()
   {
      defaultWidth = DEFAULT_WIDTH;
      defaultLeftPad = DEFAULT_LEFT_PAD;
      defaultDescPad = DEFAULT_DESC_PAD;
      defaultSyntaxPrefix = DEFAULT_SYNTAX_PREFIX;
      defaultNewLine = System.getProperty("line.separator");
      defaultOptPrefix = DEFAULT_OPT_PREFIX;
      defaultLongOptPrefix = DEFAULT_LONG_OPT_PREFIX;
      defaultArgName = DEFAULT_ARG_NAME;
   }

   // ------------------------------------------------------------------ Public

   public void printHelp( String cmdLineSyntax,
                          Options options )
   {
       printHelp( defaultWidth, cmdLineSyntax, null, options, null, false );
   }

   public void printHelp( String cmdLineSyntax,
                          Options options,
                          boolean autoUsage )
   {
       printHelp( defaultWidth, cmdLineSyntax, null, options, null, autoUsage );
   }

   public void printHelp( String cmdLineSyntax,
                          String header,
                          Options options,
                          String footer )
   {
       printHelp( cmdLineSyntax, header, options, footer, false );
   }

   public void printHelp( String cmdLineSyntax,
                          String header,
                          Options options,
                          String footer,
                          boolean autoUsage )
   {
      printHelp(defaultWidth, cmdLineSyntax, header, options, footer, autoUsage );
   }
   
   public void printHelp( int width,
                          String cmdLineSyntax,
                          String header,
                          Options options,
                          String footer )
   {
       printHelp( width, cmdLineSyntax, header, options, footer, false );
   }

   public void printHelp( int width,
                          String cmdLineSyntax,
                          String header,
                          Options options,
                          String footer,
                          boolean autoUsage )
   {
      PrintWriter pw = new PrintWriter(System.out);
      printHelp( pw, width, cmdLineSyntax, header,
                 options, defaultLeftPad, defaultDescPad, footer, autoUsage );
      pw.flush();
   }
   public void printHelp( PrintWriter pw,
                          int width,
                          String cmdLineSyntax,
                          String header,
                          Options options,
                          int leftPad,
                          int descPad,
                          String footer )
   throws IllegalArgumentException
   {
       printHelp( pw, width, cmdLineSyntax, header, options, leftPad, descPad, footer, false );
   }

   public void printHelp( PrintWriter pw,
                          int width,
                          String cmdLineSyntax,
                          String header,
                          Options options,
                          int leftPad,
                          int descPad,
                          String footer,
                          boolean autoUsage )
      throws IllegalArgumentException
   {
      if ( cmdLineSyntax == null || cmdLineSyntax.length() == 0 )
      {
         throw new IllegalArgumentException("cmdLineSyntax not provided");
      }

      if ( autoUsage ) {
          printUsage( pw, width, cmdLineSyntax, options );
      }
      else {
          printUsage( pw, width, cmdLineSyntax );
      }

      if ( header != null && header.trim().length() > 0 )
      {
         printWrapped( pw, width, header );
      }
      printOptions( pw, width, options, leftPad, descPad );
      if ( footer != null && footer.trim().length() > 0 )
      {
         printWrapped( pw, width, footer );
      }
   }

   /**
    * <p>Prints the usage statement for the specified application.</p>
    *
    * @param pw The PrintWriter to print the usage statement 
    * @param width ??
    * @param appName The application name
    * @param options The command line Options
    *
    */
   public void printUsage( PrintWriter pw, int width, String app, Options options ) 
   {
       // initialise the string buffer
       StringBuffer buff = new StringBuffer( defaultSyntaxPrefix ).append( app ).append( " " );
       
       // create a list for processed option groups
       ArrayList list = new ArrayList();

       // temp variable
       Option option;

       // iterate over the options
       for ( Iterator i = options.getOptions().iterator(); i.hasNext(); )
       {
           // get the next Option
           option = (Option) i.next();

           // check if the option is part of an OptionGroup
           OptionGroup group = options.getOptionGroup( option );

           // if the option is part of a group and the group has not already
           // been processed
           if( group != null && !list.contains(group)) {

               // add the group to the processed list
               list.add( group );

               // get the names of the options from the OptionGroup
               Collection names = group.getNames();

               buff.append( "[" ); 

               // for each option in the OptionGroup
               for( Iterator iter = names.iterator(); iter.hasNext(); ) {
                   buff.append( iter.next() );
                   if( iter.hasNext() ) {
                       buff.append( " | " );
                   }
               }
               buff.append( "]" );
           }
           // if the Option is not part of an OptionGroup
           else {
               // if the Option is not a required option
               if( !option.isRequired() ) {
                   buff.append( "[" );
               }
               
               if( !" ".equals( option.getOpt() ) ) {
                   buff.append( "-" ).append( option.getOpt() );
               }
               else {
                   buff.append( "--" ).append( option.getLongOpt() );
               }

               if( option.hasArg() ){
                   buff.append( " " );
               }

               // if the Option has a value
               if( option.hasArg() ) {
                   buff.append( option.getArgName() );
               }

               // if the Option is not a required option
               if( !option.isRequired() ) {
                   buff.append( "]" );
               }
               buff.append( " " );
           }
       }

       // call printWrapped
       printWrapped( pw, width, buff.toString().indexOf(' ')+1,
                     buff.toString() );
   }

   public void printUsage( PrintWriter pw, int width, String cmdLineSyntax )
   {
      int argPos = cmdLineSyntax.indexOf(' ') + 1;
      printWrapped(pw, width, defaultSyntaxPrefix.length() + argPos,
                   defaultSyntaxPrefix + cmdLineSyntax);
   }

   public void printOptions( PrintWriter pw, int width, Options options, int leftPad, int descPad )
   {
      StringBuffer sb = new StringBuffer();
      renderOptions(sb, width, options, leftPad, descPad);
      pw.println(sb.toString());
   }

   public void printWrapped( PrintWriter pw, int width, String text )
   {
      printWrapped(pw, width, 0, text);
   }

   public void printWrapped( PrintWriter pw, int width, int nextLineTabStop, String text )
   {
      StringBuffer sb = new StringBuffer(text.length());
      renderWrappedText(sb, width, nextLineTabStop, text);
      pw.println(sb.toString());
   }

   // --------------------------------------------------------------- Protected

   protected StringBuffer renderOptions( StringBuffer sb,
                                         int width,
                                         Options options,
                                         int leftPad,
                                         int descPad )
   {
      final String lpad = createPadding(leftPad);
      final String dpad = createPadding(descPad);

      //first create list containing only <lpad>-a,--aaa where -a is opt and --aaa is
      //long opt; in parallel look for the longest opt string
      //this list will be then used to sort options ascending
      int max = 0;
      StringBuffer optBuf;
      List prefixList = new ArrayList();
      Option option;
      List optList = options.helpOptions();
      Collections.sort( optList, new StringBufferComparator() );
      for ( Iterator i = optList.iterator(); i.hasNext(); )
      {
         option = (Option) i.next();
         optBuf = new StringBuffer(8);

         if (option.getOpt().equals(" "))
         {
             optBuf.append(lpad).append("   " + defaultLongOptPrefix).append(option.getLongOpt());
         }
         else
         {
             optBuf.append(lpad).append(defaultOptPrefix).append(option.getOpt());
             if ( option.hasLongOpt() )
             {
                optBuf.append(',').append(defaultLongOptPrefix).append(option.getLongOpt());
             }

         }

         if( option.hasArg() ) {
             if( option.hasArgName() ) {
                 optBuf.append(" <").append( option.getArgName() ).append( '>' );
             }
             else {
                 optBuf.append(' ');
             }
         }

         prefixList.add(optBuf);
         max = optBuf.length() > max ? optBuf.length() : max;
      }
      int x = 0;
      for ( Iterator i = optList.iterator(); i.hasNext(); )
      {
         option = (Option) i.next();
         optBuf = new StringBuffer( prefixList.get( x++ ).toString() );

         if ( optBuf.length() < max )
         {
             optBuf.append(createPadding(max - optBuf.length()));
         }
         optBuf.append( dpad );
         
         int nextLineTabStop = max + descPad;
         renderWrappedText(sb, width, nextLineTabStop,
                           optBuf.append(option.getDescription()).toString());
         if ( i.hasNext() )
         {
             sb.append(defaultNewLine);
         }
      }

      return sb;
   }

   protected StringBuffer renderWrappedText( StringBuffer sb,
                                             int width,
                                             int nextLineTabStop,
                                             String text )
   {
      int pos = findWrapPos( text, width, 0);
      if ( pos == -1 )
      {
         sb.append(rtrim(text));
         return sb;
      }
      else
      {
         sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
      }

      //all following lines must be padded with nextLineTabStop space characters
      final String padding = createPadding(nextLineTabStop);

      while ( true )
      {
         text = padding + text.substring(pos).trim();
         pos = findWrapPos( text, width, nextLineTabStop );
         if ( pos == -1 )
         {
            sb.append(text);
            return sb;
         }

         sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
      }

   }

   /**
    * Finds the next text wrap position after <code>startPos</code> for the text
    * in <code>sb</code> with the column width <code>width</code>.
    * The wrap point is the last postion before startPos+width having a whitespace
    * character (space, \n, \r).
    *
    * @param sb text to be analyzed
    * @param width width of the wrapped text
    * @param startPos position from which to start the lookup whitespace character
    * @return postion on which the text must be wrapped or -1 if the wrap position is at the end
    *         of the text
    */
   protected int findWrapPos( String text, int width, int startPos )
   {
      int pos = -1;
      // the line ends before the max wrap pos or a new line char found
      if ( ((pos = text.indexOf('\n', startPos)) != -1 && pos <= width)  ||
           ((pos = text.indexOf('\t', startPos)) != -1 && pos <= width) )
      {
         return pos;
      }
      else if ( (startPos + width) >= text.length() )
      {
         return -1;
      }

      //look for the last whitespace character before startPos+width
      pos = startPos + width;
      char c;
      while ( pos >= startPos && (c = text.charAt(pos)) != ' ' && c != '\n' && c != '\r' )
      {
         --pos;
      }
      //if we found it - just return
      if ( pos > startPos )
      {
         return pos;
      }
      else
      {
         //must look for the first whitespace chearacter after startPos + width
         pos = startPos + width;
         while ( pos <= text.length() && (c = text.charAt(pos)) != ' ' && c != '\n' && c != '\r' )
         {
            ++pos;
         }
         return pos == text.length() ? -1 : pos;
      }
   }

   protected String createPadding(int len)
   {
      StringBuffer sb = new StringBuffer(len);
      for ( int i = 0; i < len; ++i )
      {
         sb.append(' ');
      }
      return sb.toString();
   }

   protected String rtrim( String s )
   {
      if ( s == null || s.length() == 0 )
      {
         return s;
      }

      int pos = s.length();
      while ( pos >= 0 && Character.isWhitespace(s.charAt(pos-1)) )
      {
         --pos;
      }
      return s.substring(0, pos);
   }

   // ------------------------------------------------------- Package protected
   
   // ----------------------------------------------------------------- Private
   
   // ----------------------------------------------------------- Inner classes

    private static class StringBufferComparator
    implements Comparator
    {
        public int compare( Object o1, Object o2 )
        {
            String str1 = stripPrefix(o1.toString());
            String str2 = stripPrefix(o2.toString());
            return (str1.compareTo(str2));
        }

        private String stripPrefix(String strOption)
        {
            // Strip any leading '-' characters
            int iStartIndex = strOption.lastIndexOf('-');
            if (iStartIndex == -1)
            {
              iStartIndex = 0;
            }
            return strOption.substring(iStartIndex);

        }
    }
} public class Option implements Cloneable {

    /** constant that specifies the number of argument values has not been specified */
    public final static int UNINITIALIZED = -1;
    
    /** constant that specifies the number of argument values is infinite */
    public final static int UNLIMITED_VALUES = -2;
    
    /** opt the single character representation of the option */
    private String opt;

    /** longOpt is the long representation of the option */
    private String longOpt;

    /** hasArg specifies whether this option has an associated argument */
    private boolean hasArg;

    /** argName specifies the name of the argument for this option */
    private String argName;

    /** description of the option */
    private String description;

    /** required specifies whether this option is required to be present */
    private boolean required;

    /** specifies whether the argument value of this Option is optional */
    private boolean optionalArg;

    /** 
     * numberOfArgs specifies the number of argument values this option 
     * can have 
     */
    private int numberOfArgs = UNINITIALIZED;   

    /** the type of this Option */
    private Object type;

    /** the list of argument values **/
    private ArrayList values = new ArrayList();
    
    /** option char (only valid for single character options) */
    private char id;

    /** the character that is the value separator */
    private char valuesep;

    /**
     * <p>Validates whether <code>opt</code> is a permissable Option
     * shortOpt.  The rules that specify if the <code>opt</code>
     * is valid are:</p>
     * <ul>
     *  <li><code>opt</code> is not NULL</li>
     *  <li>a single character <code>opt</code> that is either
     *  ' '(special case), '?', '@' or a letter</li>
     *  <li>a multi character <code>opt</code> that only contains
     *  letters.</li>
     * </ul>
     *
     * @param opt The option string to validate
     * @throws IllegalArgumentException if the Option is not valid.
     */
    private void validateOption( String opt ) 
    throws IllegalArgumentException
    {
        // check that opt is not NULL
        if( opt == null ) {
            throw new IllegalArgumentException( "opt is null" );
        }
        // handle the single character opt
        else if( opt.length() == 1 ) {
            char ch = opt.charAt( 0 );
            if ( !isValidOpt( ch ) ) {
                throw new IllegalArgumentException( "illegal option value '" 
                                                    + ch + "'" );
            }
            id = ch;
        }
        // handle the multi character opt
        else {
            char[] chars = opt.toCharArray();
            for( int i = 0; i < chars.length; i++ ) {
                if( !isValidChar( chars[i] ) ) {
                    throw new IllegalArgumentException( "opt contains illegal character value '" + chars[i] + "'" );
                }
            }
        }
    }

    /**
     * <p>Returns whether the specified character is a valid Option.</p>
     *
     * @param c the option to validate
     * @return true if <code>c</code> is a letter, ' ', '?' or '@', otherwise false.
     */
    private boolean isValidOpt( char c ) {
        return ( isValidChar( c ) || c == ' ' || c == '?' || c == '@' );
    }

    /**
     * <p>Returns whether the specified character is a valid character.</p>
     *
     * @param c the character to validate
     * @return true if <code>c</code> is a letter.
     */
    private boolean isValidChar( char c ) {
        return Character.isJavaIdentifierPart( c );
    }

    /**
     * <p>Returns the id of this Option.  This is only set when the
     * Option shortOpt is a single character.  This is used for switch
     * statements.</p>
     *
     * @return the id of this Option
     */
    public int getId( ) {
        return id;
    }

    /**
     * Creates an Option using the specified parameters.
     *
     * @param opt short representation of the option
     * @param hasArg specifies whether the Option takes an argument or not
     * @param description describes the function of the option
     */
    public Option( String opt, String description ) 
    throws IllegalArgumentException
    {
        this( opt, null, false, description );
    }

    /**
     * Creates an Option using the specified parameters.
     *
     * @param opt short representation of the option
     * @param hasArg specifies whether the Option takes an argument or not
     * @param description describes the function of the option
     */
    public Option( String opt, boolean hasArg, String description ) 
    throws IllegalArgumentException
    {
        this( opt, null, hasArg, description );
    }
    
    /**
     * <p>Creates an Option using the specified parameters.</p>
     *
     * @param opt short representation of the option
     * @param longOpt the long representation of the option
     * @param hasArg specifies whether the Option takes an argument or not
     * @param description describes the function of the option
     */
    public Option( String opt, String longOpt, boolean hasArg, String description ) 
    throws IllegalArgumentException
    {
        // ensure that the option is valid
        validateOption( opt );

        this.opt          = opt;
        this.longOpt      = longOpt;

        // if hasArg is set then the number of arguments is 1
        if( hasArg ) {
            this.numberOfArgs = 1;
        }

        this.hasArg       = hasArg;
        this.description  = description;
    }
    
    /** <p>Retrieve the name of this Option.</p>
     *
     * <p>It is this String which can be used with
     * {@link CommandLine#hasOption(String opt)} and
     * {@link CommandLine#getOptionValue(String opt)} to check
     * for existence and argument.<p>
     *
     * @return The name of this option
     */
    public String getOpt() {
        return this.opt;
    }

    /**
     * <p>Retrieve the type of this Option.</p>
     * 
     * @return The type of this option
     */
    public Object getType() {
        return this.type;
    }

    /**
     * <p>Sets the type of this Option.</p>
     *
     * @param type the type of this Option
     */
    public void setType( Object type ) {
        this.type = type;
    }
    
    /** 
     * <p>Retrieve the long name of this Option.</p>
     *
     * @return Long name of this option, or null, if there is no long name
     */
    public String getLongOpt() {
        return this.longOpt;
    }

    /**
     * <p>Sets the long name of this Option.</p>
     *
     * @param longOpt the long name of this Option
     */
    public void setLongOpt( String longOpt ) {
        this.longOpt = longOpt;
    }

    /**
     * <p>Sets whether this Option can have an optional argument.</p>
     *
     * @param optionalArg specifies whether the Option can have
     * an optional argument.
     */
    public void setOptionalArg( boolean optionalArg ) {
        this.optionalArg = optionalArg;
    }

    /**
     * @return whether this Option can have an optional argument
     */
    public boolean hasOptionalArg( ) {
        return this.optionalArg;
    }
    
    /** <p>Query to see if this Option has a long name</p>
     *
     * @return boolean flag indicating existence of a long name
     */
    public boolean hasLongOpt() {
        return ( this.longOpt != null );
    }
    
    /** <p>Query to see if this Option requires an argument</p>
     *
     * @return boolean flag indicating if an argument is required
     */
    public boolean hasArg() {
        return this.numberOfArgs > 0 || numberOfArgs == UNLIMITED_VALUES;
    }
    
    /** <p>Retrieve the self-documenting description of this Option</p>
     *
     * @return The string description of this option
     */
    public String getDescription() {
        return this.description;
    }

     /** 
      * <p>Query to see if this Option requires an argument</p>
      *
      * @return boolean flag indicating if an argument is required
      */
     public boolean isRequired() {
         return this.required;
     }

     /**
      * <p>Sets whether this Option is mandatory.</p>
      *
      * @param required specifies whether this Option is mandatory
      */
     public void setRequired( boolean required ) {
         this.required = required;
     }

     /**
      * <p>Sets the display name for the argument value.</p>
      *
      * @param argName the display name for the argument value.
      */
     public void setArgName( String argName ) {
         this.argName = argName;
     }

     /**
      * <p>Gets the display name for the argument value.</p>
      *
      * @return the display name for the argument value.
      */
     public String getArgName() {
         return this.argName;
     }

     /**
      * <p>Returns whether the display name for the argument value
      * has been set.</p>
      *
      * @return if the display name for the argument value has been
      * set.
      */
     public boolean hasArgName() {
         return (this.argName != null && this.argName.length() > 0 );
     }

     /** 
      * <p>Query to see if this Option can take many values</p>
      *
      * @return boolean flag indicating if multiple values are allowed
      */
     public boolean hasArgs() {
         return ( this.numberOfArgs > 1 || this.numberOfArgs == UNLIMITED_VALUES );
     }

     /** 
      * <p>Sets the number of argument values this Option can take.</p>
      *
      * @param num the number of argument values
      */
     public void setArgs( int num ) {
         this.numberOfArgs = num;
     }

     /**
      * <p>Sets the value separator.  For example if the argument value
      * was a Java property, the value separator would be '='.</p>
      *
      * @param sep The value separator.
      */
     public void setValueSeparator( char sep ) {
         this.valuesep = sep;
     }

     /**
      * <p>Returns the value separator character.</p>
      *
      * @return the value separator character.
      */
     public char getValueSeparator() {
         return this.valuesep;
     }

     /** 
      * <p>Returns the number of argument values this Option can take.</p>
      *
      * @return num the number of argument values
      */
     public int getArgs( ) {
         return this.numberOfArgs;
     }

    /** 
     * <p>Dump state, suitable for debugging.</p>
     *
     * @return Stringified form of this object
     */
    public String toString() {
        StringBuffer buf = new StringBuffer().append("[ option: ");
        
        buf.append( this.opt );
        
        if ( this.longOpt != null ) {
            buf.append(" ")
            .append(this.longOpt);
        }
        
        buf.append(" ");
        
        if ( hasArg ) {
            buf.append( "+ARG" );
        }
        
        buf.append(" :: ")
        .append( this.description );
        
        if ( this.type != null ) {
            buf.append(" :: ")
            .append( this.type );
        }

        buf.append(" ]");
        return buf.toString();
    }

    /**
     * <p>Adds the specified value to this Option.</p>
     * 
     * @param value is a/the value of this Option
     */
    public boolean addValue( String value ) {

        switch( numberOfArgs ) {
            case UNINITIALIZED:
                return false;
            case UNLIMITED_VALUES:
                if( getValueSeparator() > 0 ) {
                    int index = 0;
                    while( (index = value.indexOf( getValueSeparator() ) ) != -1 ) {
                        this.values.add( value.substring( 0, index ) );
                        value = value.substring( index+1 );
                    }
                }
                this.values.add( value );
                return true;
            default:
                if( getValueSeparator() > 0 ) {
                    int index = 0;
                    while( (index = value.indexOf( getValueSeparator() ) ) != -1 ) {
                        if( values.size() > numberOfArgs-1 ) {
                            return false;
                        }
                        this.values.add( value.substring( 0, index ) );
                        value = value.substring( index+1 );
                    }
                }
                if( values.size() > numberOfArgs-1 ) {
                    return false;
                }
                this.values.add( value );
                return true;
        }
    }

    /**
     * @return the value/first value of this Option or 
     * <code>null</code> if there are no values.
     */
    public String getValue() {
        return this.values.size()==0 ? null : (String)this.values.get( 0 );
    }

    /**
     * @return the specified value of this Option or 
     * <code>null</code> if there are no values.
     */
    public String getValue( int index ) 
    throws IndexOutOfBoundsException
    {
        return ( this.values.size()==0 ) ? null : (String)this.values.get( index );
    }

    /**
     * @return the value/first value of this Option or the 
     * <code>defaultValue</code> if there are no values.
     */
    public String getValue( String defaultValue ) {
        String value = getValue( );
        return ( value != null ) ? value : defaultValue;
    }

    /**
     * @return the values of this Option as a String array 
     * or null if there are no values
     */
    public String[] getValues() {
        return this.values.size()==0 ? null : (String[])this.values.toArray(new String[]{});
    }

    /**
     * @return the values of this Option as a List
     * or null if there are no values
     */
    public java.util.List getValuesList() {
        return this.values;
    }

    /**
     * @return a copy of this Option
     */
    public Object clone() {
        Option option = new Option( getOpt(), getDescription() );
        option.setArgs( getArgs() );
        option.setOptionalArg( hasOptionalArg() );
        option.setRequired( isRequired() );
        option.setLongOpt( getLongOpt() );
        option.setType( getType() );
        option.setValueSeparator( getValueSeparator() );
        return option;
    }
} public class Options {

    /** a map of the options with the character key */
    private Map  shortOpts    = new HashMap();

    /** a map of the options with the long key */
    private Map  longOpts     = new HashMap();

    /** a map of the required options */
    private List requiredOpts = new ArrayList();
    
    /** a map of the option groups */
    private Map optionGroups  = new HashMap();

    /** <p>Construct a new Options descriptor</p>
     */
    public Options() {        
    }

    /**
     * <p>Add the specified option group.</p>
     *
     * @param group the OptionGroup that is to be added
     * @return the resulting Options instance
     */
    public Options addOptionGroup( OptionGroup group ) {
        Iterator options = group.getOptions().iterator();

        if( group.isRequired() ) {
            requiredOpts.add( group );
        }

        while( options.hasNext() ) {
            Option option = (Option)options.next();
            // an Option cannot be required if it is in an
            // OptionGroup, either the group is required or
            // nothing is required
            option.setRequired( false );
            addOption( option );

            optionGroups.put( option.getOpt(), group );
        }

        return this;
    }

    /** <p>Add an option that only contains a short-name</p>
     * <p>It may be specified as requiring an argument.</p>
     *
     * @param opt Short single-character name of the option.
     * @param hasArg flag signally if an argument is required after this option
     * @param description Self-documenting description
     * @return the resulting Options instance
     */
    public Options addOption(String opt, boolean hasArg, String description) {
        addOption( opt, null, hasArg, description );
        return this;
    }
    
    /** <p>Add an option that contains a short-name and a long-name</p>
     * <p>It may be specified as requiring an argument.</p>
     *
     * @param opt Short single-character name of the option.
     * @param longOpt Long multi-character name of the option.
     * @param hasArg flag signally if an argument is required after this option
     * @param description Self-documenting description
     * @return the resulting Options instance
     */
    public Options addOption(String opt, String longOpt, boolean hasArg, String description) {
        addOption( new Option( opt, longOpt, hasArg, description ) );        
        return this;
    }

    /**
     * <p>Adds an option instance</p>
     *
     * @param opt the option that is to be added 
     * @return the resulting Options instance
     */
    public Options addOption(Option opt)  {
        String shortOpt = "-" + opt.getOpt();
        
        // add it to the long option list
        if ( opt.hasLongOpt() ) {
            longOpts.put( "--" + opt.getLongOpt(), opt );
        }
        
        // if the option is required add it to the required list
        if ( opt.isRequired() ) {
            requiredOpts.add( shortOpt );
        }

        shortOpts.put( shortOpt, opt );
        
        return this;
    }
    
    /** <p>Retrieve a read-only list of options in this set</p>
     *
     * @return read-only Collection of {@link Option} objects in this descriptor
     */
    public Collection getOptions() {
        List opts = new ArrayList( shortOpts.values() );

        // now look through the long opts to see if there are any Long-opt
        // only options
        Iterator iter = longOpts.values().iterator();
        while (iter.hasNext())
        {
            Object item = iter.next();
            if (!opts.contains(item))
            {
                opts.add(item);
            }
        }
        return Collections.unmodifiableCollection( opts );
    }

    /**
     * <p>Returns the Options for use by the HelpFormatter.</p>
     *
     * @return the List of Options
     */
    List helpOptions() {
        return new ArrayList( shortOpts.values() );
    }

    /** <p>Returns the required options as a 
     * <code>java.util.Collection</code>.</p>
     *
     * @return Collection of required options
     */
    public List getRequiredOptions() {
        return requiredOpts;
    }
    
    /** <p>Retrieve the named {@link Option}</p>
     *
     * @param opt short or long name of the {@link Option}
     * @return the option represented by opt
     */
    public Option getOption( String opt ) {

        Option option = null;

        // short option
        if( opt.length() == 1 ) {
            option = (Option)shortOpts.get( "-" + opt );
        }
        // long option
        else if( opt.startsWith( "--" ) ) {
            option = (Option)longOpts.get( opt );
        }
        // a just-in-case
        else {
            option = (Option)shortOpts.get( opt );
        }

        return (option == null) ? null : (Option)option.clone();
    }

    /** 
     * <p>Returns whether the named {@link Option} is a member
     * of this {@link Options}</p>
     *
     * @param opt short or long name of the {@link Option}
     * @return true if the named {@link Option} is a member
     * of this {@link Options}
     */
    public boolean hasOption( String opt ) {

        // short option
        if( opt.length() == 1 ) {
            return shortOpts.containsKey( "-" + opt );
        }
        // long option
        else if( opt.startsWith( "--" ) ) {
            return longOpts.containsKey( opt );
        }
        // a just-in-case
        else {
            return shortOpts.containsKey( opt );
        }
    }

    /** <p>Returns the OptionGroup the <code>opt</code>
     * belongs to.</p>
     * @param opt the option whose OptionGroup is being queried.
     *
     * @return the OptionGroup if <code>opt</code> is part
     * of an OptionGroup, otherwise return null
     */
    public OptionGroup getOptionGroup( Option opt ) {
        return (OptionGroup)optionGroups.get( opt.getOpt() );
    }
    
    /** <p>Dump state, suitable for debugging.</p>
     *
     * @return Stringified form of this object
     */
    public String toString() {
        StringBuffer buf = new StringBuffer();
        
        buf.append("[ Options: [ short ");
        buf.append( shortOpts.toString() );
        buf.append( " ] [ long " );
        buf.append( longOpts );
        buf.append( " ]");
        
        return buf.toString();
    }
} public class ParseException extends Exception 
{
    
    /** 
     * <p>Construct a new <code>ParseException</code> 
     * with the specified detail message.</p>
     *
     * @param message the detail message
     */
    public ParseException( String message ) {
        super( message );
    }
} public class TarEntry
{
    /**
     * The length of the name field in a header buffer.
     */
    public static final int NAMELEN = 100;

    /**
     * The entry's modification time.
     */
    private int m_checkSum;

    /**
     * The entry's group name.
     */
    private int m_devMajor;

    /**
     * The entry's major device number.
     */
    private int m_devMinor;

    /**
     * The entry's minor device number.
     */
    private File m_file;

    /**
     * The entry's user id.
     */
    private int m_groupID;

    /**
     * The entry's user name.
     */
    private StringBuffer m_groupName;

    /**
     * The entry's checksum.
     */
    private byte m_linkFlag;

    /**
     * The entry's link flag.
     */
    private StringBuffer m_linkName;

    /**
     * The entry's link name.
     */
    private StringBuffer m_magic;

    /**
     * The entry's size.
     */
    private long m_modTime;

    /**
     * The entry's name.
     */
    private int m_mode;

    private StringBuffer m_name;

    /**
     * The entry's group id.
     */
    private long m_size;

    /**
     * The entry's permission mode.
     */
    private int m_userID;

    /**
     * The entry's magic tag.
     */
    private StringBuffer m_userName;

    /**
     * Construct an entry with only a name. This allows the programmer to
     * construct the entry's header "by hand". File is set to null.
     *
     * @param name the name of the entry
     */
    public TarEntry( final String name )
    {
        this();

        final boolean isDir = name.endsWith( "/" );

        m_name = new StringBuffer( name );
        m_mode = isDir ? 040755 : 0100644;
        m_linkFlag = isDir ? TarConstants.LF_DIR : TarConstants.LF_NORMAL;
        m_modTime = ( new Date() ).getTime() / 1000;
        m_linkName = new StringBuffer( "" );
        m_userName = new StringBuffer( "" );
        m_groupName = new StringBuffer( "" );
    }

    /**
     * Construct an entry with a name an a link flag.
     *
     * @param name Description of Parameter
     * @param linkFlag Description of Parameter
     */
    public TarEntry( final String name, final byte linkFlag )
    {
        this( name );
        m_linkFlag = linkFlag;
    }

    /**
     * Construct an entry for a file. File is set to file, and the header is
     * constructed from information from the file.
     *
     * @param file The file that the entry represents.
     */
    public TarEntry( final File file )
    {
        this();

        m_file = file;

        String name = file.getPath();

        // Strip off drive letters!
        final String osName =
            System.getProperty( "os.name" ).toLowerCase( Locale.US );
        if( -1 != osName.indexOf( "netware" ) )
        {
            if( name.length() > 2 )
            {
                final char ch1 = name.charAt( 0 );
                final char ch2 = name.charAt( 1 );

                if( ch2 == ':' &&
                    ( ( ch1 >= 'a' && ch1 <= 'z' ) ||
                    ( ch1 >= 'A' && ch1 <= 'Z' ) ) )
                {
                    name = name.substring( 2 );
                }
            }
        }
        else if( -1 != osName.indexOf( "netware" ) )
        {
            final int colon = name.indexOf( ':' );
            if( colon != -1 )
            {
                name = name.substring( colon + 1 );
            }
        }

        name = name.replace( File.separatorChar, '/' );

        // No absolute pathnames
        // Windows (and Posix?) paths can start with "\\NetworkDrive\",
        // so we loop on starting /'s.
        while( name.startsWith( "/" ) )
        {
            name = name.substring( 1 );
        }

        m_linkName = new StringBuffer( "" );
        m_name = new StringBuffer( name );

        if( file.isDirectory() )
        {
            m_mode = 040755;
            m_linkFlag = TarConstants.LF_DIR;

            if( m_name.charAt( m_name.length() - 1 ) != '/' )
            {
                m_name.append( "/" );
            }
        }
        else
        {
            m_mode = 0100644;
            m_linkFlag = TarConstants.LF_NORMAL;
        }

        m_size = file.length();
        m_modTime = file.lastModified() / 1000;
        m_checkSum = 0;
        m_devMajor = 0;
        m_devMinor = 0;
    }

    /**
     * Construct an entry from an archive's header bytes. File is set to null.
     *
     * @param header The header bytes from a tar archive entry.
     */
    public TarEntry( final byte[] header )
    {
        this();
        parseTarHeader( header );
    }

    /**
     * Construct an empty entry and prepares the header values.
     */
    private TarEntry()
    {
        m_magic = new StringBuffer( TarConstants.TMAGIC );
        m_name = new StringBuffer();
        m_linkName = new StringBuffer();

        String user = System.getProperty( "user.name", "" );
        if( user.length() > 31 )
        {
            user = user.substring( 0, 31 );
        }

        m_userName = new StringBuffer( user );
        m_groupName = new StringBuffer( "" );
    }

    /**
     * Set this entry's group id.
     *
     * @param groupId This entry's new group id.
     */
    public void setGroupID( final int groupId )
    {
        m_groupID = groupId;
    }

    /**
     * Set this entry's group id.
     *
     * @param groupId This entry's new group id.
     * @deprecated Use setGroupID() instead
     * @see #setGroupID(int)
     */
    public void setGroupId( final int groupId )
    {
        m_groupID = groupId;
    }

    /**
     * Set this entry's group name.
     *
     * @param groupName This entry's new group name.
     */
    public void setGroupName( final String groupName )
    {
        m_groupName = new StringBuffer( groupName );
    }

    /**
     * Set this entry's modification time. The parameter passed to this method
     * is in "Java time".
     *
     * @param time This entry's new modification time.
     */
    public void setModTime( final long time )
    {
        m_modTime = time / 1000;
    }

    /**
     * Set this entry's modification time.
     *
     * @param time This entry's new modification time.
     */
    public void setModTime( final Date time )
    {
        m_modTime = time.getTime() / 1000;
    }

    /**
     * Set the mode for this entry
     *
     * @param mode The new Mode value
     */
    public void setMode( final int mode )
    {
        m_mode = mode;
    }

    /**
     * Set this entry's name.
     *
     * @param name This entry's new name.
     */
    public void setName( final String name )
    {
        m_name = new StringBuffer( name );
    }

    /**
     * Set this entry's file size.
     *
     * @param size This entry's new file size.
     */
    public void setSize( final long size )
    {
        m_size = size;
    }

    /**
     * Set this entry's user id.
     *
     * @param userId This entry's new user id.
     */
    public void setUserID( final int userId )
    {
        m_userID = userId;
    }

    /**
     * Set this entry's user id.
     *
     * @param userId This entry's new user id.
     * @deprecated Use setUserID() instead
     * @see #setUserID(int)
     */
    public void setUserId( final int userId )
    {
        m_userID = userId;
    }

    /**
     * Set this entry's user name.
     *
     * @param userName This entry's new user name.
     */
    public void setUserName( final String userName )
    {
        m_userName = new StringBuffer( userName );
    }

    /**
     * If this entry represents a file, and the file is a directory, return an
     * array of TarEntries for this entry's children.
     *
     * @return An array of TarEntry's for this entry's children.
     */
    public TarEntry[] getDirectoryEntries()
    {
        if( null == m_file || !m_file.isDirectory() )
        {
            return new TarEntry[ 0 ];
        }

        final String[] list = m_file.list();
        final TarEntry[] result = new TarEntry[ list.length ];

        for( int i = 0; i < list.length; ++i )
        {
            result[ i ] = new TarEntry( new File( m_file, list[ i ] ) );
        }

        return result;
    }

    /**
     * Get this entry's file.
     *
     * @return This entry's file.
     */
    public File getFile()
    {
        return m_file;
    }

    /**
     * Get this entry's group id.
     *
     * @return This entry's group id.
     * @deprecated Use getGroupID() instead
     * @see #getGroupID()
     */
    public int getGroupId()
    {
        return m_groupID;
    }

    /**
     * Get this entry's group id.
     *
     * @return This entry's group id.
     */
    public int getGroupID()
    {
        return m_groupID;
    }

    /**
     * Get this entry's group name.
     *
     * @return This entry's group name.
     */
    public String getGroupName()
    {
        return m_groupName.toString();
    }

    /**
     * Set this entry's modification time.
     *
     * @return The ModTime value
     */
    public Date getModTime()
    {
        return new Date( m_modTime * 1000 );
    }

    /**
     * Get this entry's mode.
     *
     * @return This entry's mode.
     */
    public int getMode()
    {
        return m_mode;
    }

    /**
     * Get this entry's name.
     *
     * @return This entry's name.
     */
    public String getName()
    {
        return m_name.toString();
    }

    /**
     * Get this entry's file size.
     *
     * @return This entry's file size.
     */
    public long getSize()
    {
        return m_size;
    }

    /**
     * Get this entry's checksum.
     *
     * @return This entry's checksum.
     */
    public int getCheckSum()
    {
        return m_checkSum;
    }

    /**
     * Get this entry's user id.
     *
     * @return This entry's user id.
     * @deprecated Use getUserID() instead
     * @see #getUserID()
     */
    public int getUserId()
    {
        return m_userID;
    }

    /**
     * Get this entry's user id.
     *
     * @return This entry's user id.
     */
    public int getUserID()
    {
        return m_userID;
    }

    /**
     * Get this entry's user name.
     *
     * @return This entry's user name.
     */
    public String getUserName()
    {
        return m_userName.toString();
    }

    /**
     * Determine if the given entry is a descendant of this entry. Descendancy
     * is determined by the name of the descendant starting with this entry's
     * name.
     *
     * @param desc Entry to be checked as a descendent of
     * @return True if entry is a descendant of
     */
    public boolean isDescendent( final TarEntry desc )
    {
        return desc.getName().startsWith( getName() );
    }

    /**
     * Return whether or not this entry represents a directory.
     *
     * @return True if this entry is a directory.
     */
    public boolean isDirectory()
    {
        if( m_file != null )
        {
            return m_file.isDirectory();
        }

        if( m_linkFlag == TarConstants.LF_DIR )
        {
            return true;
        }

        if( getName().endsWith( "/" ) )
        {
            return true;
        }

        return false;
    }

    /**
     * Indicate if this entry is a GNU long name block
     *
     * @return true if this is a long name extension provided by GNU tar
     */
    public boolean isGNULongNameEntry()
    {
        return m_linkFlag == TarConstants.LF_GNUTYPE_LONGNAME &&
            m_name.toString().equals( TarConstants.GNU_LONGLINK );
    }

    /**
     * Determine if the two entries are equal. Equality is determined by the
     * header names being equal.
     *
     * @param other Entry to be checked for equality.
     * @return True if the entries are equal.
     */
    public boolean equals( final TarEntry other )
    {
        return getName().equals( other.getName() );
    }

    /**
     * Parse an entry's header information from a header buffer.
     *
     * @param header The tar entry header buffer to get information from.
     */
    private void parseTarHeader( final byte[] header )
    {
        int offset = 0;

        m_name = TarUtils.parseName( header, offset, NAMELEN );
        offset += NAMELEN;
        m_mode = (int)TarUtils.parseOctal( header, offset, TarConstants.MODELEN );
        offset += TarConstants.MODELEN;
        m_userID = (int)TarUtils.parseOctal( header, offset, TarConstants.UIDLEN );
        offset += TarConstants.UIDLEN;
        m_groupID = (int)TarUtils.parseOctal( header, offset, TarConstants.GIDLEN );
        offset += TarConstants.GIDLEN;
        m_size = TarUtils.parseOctal( header, offset, TarConstants.SIZELEN );
        offset += TarConstants.SIZELEN;
        m_modTime = TarUtils.parseOctal( header, offset, TarConstants.MODTIMELEN );
        offset += TarConstants.MODTIMELEN;
        m_checkSum = (int)TarUtils.parseOctal( header, offset, TarConstants.CHKSUMLEN );
        offset += TarConstants.CHKSUMLEN;
        m_linkFlag = header[ offset++ ];
        m_linkName = TarUtils.parseName( header, offset, NAMELEN );
        offset += NAMELEN;
        m_magic = TarUtils.parseName( header, offset, TarConstants.MAGICLEN );
        offset += TarConstants.MAGICLEN;
        m_userName = TarUtils.parseName( header, offset, TarConstants.UNAMELEN );
        offset += TarConstants.UNAMELEN;
        m_groupName = TarUtils.parseName( header, offset, TarConstants.GNAMELEN );
        offset += TarConstants.GNAMELEN;
        m_devMajor = (int)TarUtils.parseOctal( header, offset, TarConstants.DEVLEN );
        offset += TarConstants.DEVLEN;
        m_devMinor = (int)TarUtils.parseOctal( header, offset, TarConstants.DEVLEN );
    }

    /**
     * Write an entry's header information to a header buffer.
     *
     * @param buffer The tar entry header buffer to fill in.
     */
    public void writeEntryHeader( final byte[] buffer )
    {
        int offset = 0;

        offset = TarUtils.getNameBytes( m_name, buffer, offset, NAMELEN );
        offset = TarUtils.getOctalBytes( m_mode, buffer, offset, TarConstants.MODELEN );
        offset = TarUtils.getOctalBytes( m_userID, buffer, offset, TarConstants.UIDLEN );
        offset = TarUtils.getOctalBytes( m_groupID, buffer, offset, TarConstants.GIDLEN );
        offset = TarUtils.getLongOctalBytes( m_size, buffer, offset, TarConstants.SIZELEN );
        offset = TarUtils.getLongOctalBytes( m_modTime, buffer, offset, TarConstants.MODTIMELEN );

        final int checkSumOffset = offset;
        for( int i = 0; i < TarConstants.CHKSUMLEN; ++i )
        {
            buffer[ offset++ ] = (byte)' ';
        }

        buffer[ offset++ ] = m_linkFlag;
        offset = TarUtils.getNameBytes( m_linkName, buffer, offset, NAMELEN );
        offset = TarUtils.getNameBytes( m_magic, buffer, offset, TarConstants.MAGICLEN );
        offset = TarUtils.getNameBytes( m_userName, buffer, offset, TarConstants.UNAMELEN );
        offset = TarUtils.getNameBytes( m_groupName, buffer, offset, TarConstants.GNAMELEN );
        offset = TarUtils.getOctalBytes( m_devMajor, buffer, offset, TarConstants.DEVLEN );
        offset = TarUtils.getOctalBytes( m_devMinor, buffer, offset, TarConstants.DEVLEN );

        while( offset < buffer.length )
        {
            buffer[ offset++ ] = 0;
        }

        final long checkSum = TarUtils.computeCheckSum( buffer );
        TarUtils.getCheckSumOctalBytes( checkSum, buffer, checkSumOffset, TarConstants.CHKSUMLEN );
    }
} public class TarInputStream
    extends FilterInputStream
{
    private TarBuffer m_buffer;
    private TarEntry m_currEntry;
    private boolean m_debug;
    private int m_entryOffset;
    private int m_entrySize;
    private boolean m_hasHitEOF;
    private byte[] m_oneBuf;
    private byte[] m_readBuf;

    /**
     * Construct a TarInputStream using specified input
     * stream and default block and record sizes.
     *
     * @param input stream to create TarInputStream from
     * @see TarBuffer#DEFAULT_BLOCKSIZE
     * @see TarBuffer#DEFAULT_RECORDSIZE
     */
    public TarInputStream( final InputStream input )
    {
        this( input, TarBuffer.DEFAULT_BLOCKSIZE, TarBuffer.DEFAULT_RECORDSIZE );
    }

    /**
     * Construct a TarInputStream using specified input
     * stream, block size and default record sizes.
     *
     * @param input stream to create TarInputStream from
     * @param blockSize the block size to use
     * @see TarBuffer#DEFAULT_RECORDSIZE
     */
    public TarInputStream( final InputStream input,
                           final int blockSize )
    {
        this( input, blockSize, TarBuffer.DEFAULT_RECORDSIZE );
    }

    /**
     * Construct a TarInputStream using specified input
     * stream, block size and record sizes.
     *
     * @param input stream to create TarInputStream from
     * @param blockSize the block size to use
     * @param recordSize the record size to use
     */
    public TarInputStream( final InputStream input,
                           final int blockSize,
                           final int recordSize )
    {
        super( input );

        m_buffer = new TarBuffer( input, blockSize, recordSize );
        m_oneBuf = new byte[ 1 ];
    }

    /**
     * Sets the debugging flag.
     *
     * @param debug The new Debug value
     */
    public void setDebug( final boolean debug )
    {
        m_debug = debug;
        m_buffer.setDebug( debug );
    }

    /**
     * Get the next entry in this tar archive. This will skip over any remaining
     * data in the current entry, if there is one, and place the input stream at
     * the header of the next entry, and read the header and instantiate a new
     * TarEntry from the header bytes and return that entry. If there are no
     * more entries in the archive, null will be returned to indicate that the
     * end of the archive has been reached.
     *
     * @return The next TarEntry in the archive, or null.
     * @exception IOException Description of Exception
     */
    public TarEntry getNextEntry()
        throws IOException
    {
        if( m_hasHitEOF )
        {
            return null;
        }

        if( m_currEntry != null )
        {
            final int numToSkip = m_entrySize - m_entryOffset;

            if( m_debug )
            {
                final String message = "TarInputStream: SKIP currENTRY '" +
                    m_currEntry.getName() + "' SZ " + m_entrySize +
                    " OFF " + m_entryOffset + "  skipping " + numToSkip + " bytes";
                debug( message );
            }

            if( numToSkip > 0 )
            {
                skip( numToSkip );
            }

            m_readBuf = null;
        }

        final byte[] headerBuf = m_buffer.readRecord();
        if( headerBuf == null )
        {
            if( m_debug )
            {
                debug( "READ NULL RECORD" );
            }
            m_hasHitEOF = true;
        }
        else if( m_buffer.isEOFRecord( headerBuf ) )
        {
            if( m_debug )
            {
                debug( "READ EOF RECORD" );
            }
            m_hasHitEOF = true;
        }

        if( m_hasHitEOF )
        {
            m_currEntry = null;
        }
        else
        {
            m_currEntry = new TarEntry( headerBuf );

            if( !( headerBuf[ 257 ] == 'u' && headerBuf[ 258 ] == 's' &&
                headerBuf[ 259 ] == 't' && headerBuf[ 260 ] == 'a' &&
                headerBuf[ 261 ] == 'r' ) )
            {
                //Must be v7Format
            }

            if( m_debug )
            {
                final String message = "TarInputStream: SET CURRENTRY '" +
                    m_currEntry.getName() + "' size = " + m_currEntry.getSize();
                debug( message );
            }

            m_entryOffset = 0;

            // REVIEW How do we resolve this discrepancy?!
            m_entrySize = (int)m_currEntry.getSize();
        }

        if( null != m_currEntry && m_currEntry.isGNULongNameEntry() )
        {
            // read in the name
            final StringBuffer longName = new StringBuffer();
            final byte[] buffer = new byte[ 256 ];
            int length = 0;
            while( ( length = read( buffer ) ) >= 0 )
            {
                final String str = new String( buffer, 0, length );
                longName.append( str );
            }
            getNextEntry();

            // remove trailing null terminator
            if (longName.length() > 0
                && longName.charAt(longName.length() - 1) == 0) {
                longName.deleteCharAt(longName.length() - 1);
            }
            
            m_currEntry.setName( longName.toString() );
        }

        return m_currEntry;
    }

    /**
     * Get the record size being used by this stream's TarBuffer.
     *
     * @return The TarBuffer record size.
     */
    public int getRecordSize()
    {
        return m_buffer.getRecordSize();
    }

    /**
     * Get the available data that can be read from the current entry in the
     * archive. This does not indicate how much data is left in the entire
     * archive, only in the current entry. This value is determined from the
     * entry's size header field and the amount of data already read from the
     * current entry.
     *
     * @return The number of available bytes for the current entry.
     * @exception IOException when an IO error causes operation to fail
     */
    public int available()
        throws IOException
    {
        return m_entrySize - m_entryOffset;
    }

    /**
     * Closes this stream. Calls the TarBuffer's close() method.
     *
     * @exception IOException when an IO error causes operation to fail
     */
    public void close()
        throws IOException
    {
        m_buffer.close();
    }

    /**
     * Copies the contents of the current tar archive entry directly into an
     * output stream.
     *
     * @param output The OutputStream into which to write the entry's data.
     * @exception IOException when an IO error causes operation to fail
     */
    public void copyEntryContents( final OutputStream output )
        throws IOException
    {
        final byte[] buffer = new byte[ 32 * 1024 ];
        while( true )
        {
            final int numRead = read( buffer, 0, buffer.length );
            if( numRead == -1 )
            {
                break;
            }

            output.write( buffer, 0, numRead );
        }
    }

    /**
     * Since we do not support marking just yet, we do nothing.
     *
     * @param markLimit The limit to mark.
     */
    public void mark( int markLimit )
    {
    }

    /**
     * Since we do not support marking just yet, we return false.
     *
     * @return False.
     */
    public boolean markSupported()
    {
        return false;
    }

    /**
     * Reads a byte from the current tar archive entry. This method simply calls
     * read( byte[], int, int ).
     *
     * @return The byte read, or -1 at EOF.
     * @exception IOException when an IO error causes operation to fail
     */
    public int read()
        throws IOException
    {
        final int num = read( m_oneBuf, 0, 1 );
        if( num == -1 )
        {
            return num;
        }
        else
        {
            return (int)m_oneBuf[ 0 ];
        }
    }

    /**
     * Reads bytes from the current tar archive entry. This method simply calls
     * read( byte[], int, int ).
     *
     * @param buffer The buffer into which to place bytes read.
     * @return The number of bytes read, or -1 at EOF.
     * @exception IOException when an IO error causes operation to fail
     */
    public int read( final byte[] buffer )
        throws IOException
    {
        return read( buffer, 0, buffer.length );
    }

    /**
     * Reads bytes from the current tar archive entry. This method is aware of
     * the boundaries of the current entry in the archive and will deal with
     * them as if they were this stream's start and EOF.
     *
     * @param buffer The buffer into which to place bytes read.
     * @param offset The offset at which to place bytes read.
     * @param count The number of bytes to read.
     * @return The number of bytes read, or -1 at EOF.
     * @exception IOException when an IO error causes operation to fail
     */
    public int read( final byte[] buffer,
                     final int offset,
                     final int count )
        throws IOException
    {
        int position = offset;
        int numToRead = count;
        int totalRead = 0;

        if( m_entryOffset >= m_entrySize )
        {
            return -1;
        }

        if( ( numToRead + m_entryOffset ) > m_entrySize )
        {
            numToRead = ( m_entrySize - m_entryOffset );
        }

        if( null != m_readBuf )
        {
            final int size =
                ( numToRead > m_readBuf.length ) ? m_readBuf.length : numToRead;

            System.arraycopy( m_readBuf, 0, buffer, position, size );

            if( size >= m_readBuf.length )
            {
                m_readBuf = null;
            }
            else
            {
                final int newLength = m_readBuf.length - size;
                final byte[] newBuffer = new byte[ newLength ];

                System.arraycopy( m_readBuf, size, newBuffer, 0, newLength );

                m_readBuf = newBuffer;
            }

            totalRead += size;
            numToRead -= size;
            position += size;
        }

        while( numToRead > 0 )
        {
            final byte[] rec = m_buffer.readRecord();
            if( null == rec )
            {
                // Unexpected EOF!
                final String message =
                    "unexpected EOF with " + numToRead + " bytes unread";
                throw new IOException( message );
            }

            int size = numToRead;
            final int recordLength = rec.length;

            if( recordLength > size )
            {
                System.arraycopy( rec, 0, buffer, position, size );

                m_readBuf = new byte[ recordLength - size ];

                System.arraycopy( rec, size, m_readBuf, 0, recordLength - size );
            }
            else
            {
                size = recordLength;

                System.arraycopy( rec, 0, buffer, position, recordLength );
            }

            totalRead += size;
            numToRead -= size;
            position += size;
        }

        m_entryOffset += totalRead;

        return totalRead;
    }

    /**
     * Since we do not support marking just yet, we do nothing.
     */
    public void reset()
    {
    }

    /**
     * Skip bytes in the input buffer. This skips bytes in the current entry's
     * data, not the entire archive, and will stop at the end of the current
     * entry's data if the number to skip extends beyond that point.
     *
     * @param numToSkip The number of bytes to skip.
     * @exception IOException when an IO error causes operation to fail
     */
    public void skip( final int numToSkip )
        throws IOException
    {
        // REVIEW
        // This is horribly inefficient, but it ensures that we
        // properly skip over bytes via the TarBuffer...
        //
        final byte[] skipBuf = new byte[ 8 * 1024 ];
        int num = numToSkip;
        while( num > 0 )
        {
            final int count = ( num > skipBuf.length ) ? skipBuf.length : num;
            final int numRead = read( skipBuf, 0, count );
            if( numRead == -1 )
            {
                break;
            }

            num -= numRead;
        }
    }

    /**
     * Utility method to do debugging.
     * Capable of being overidden in sub-classes.
     *
     * @param message the message to use in debugging
     */
    protected void debug( final String message )
    {
        if( m_debug )
        {
            System.err.println( message );
        }
    }
} public class PsStats {
	/** A list of processes (with enclosing CPU samples). */
	public List processList;
	/** Statistics sampling period. */
	public int samplePeriod;
	/** Start and end of sampling period. */
	public Date startTime;
	public Date endTime;
} public class Stats {
	/** A list of statistics samples. */
	private List samples;
	
	/**
	 * Creates a new  statistics instance.
	 */
	public Stats() {
		samples = new ArrayList();
	}

	/**
	 * Adds a new statistics sample.
	 * 
	 * @param sample  statistics sample to add
	 */
	public void addSample(Sample sample) {
		samples.add(sample);
	}
	
	/**
	 * Returns a list of statistics samples.
	 * 
	 * @return  a list statistics samples
	 */
	public List getSamples() {
		return samples;
	}
} public class PacctParser {
	private static final Logger log = Logger.getLogger(PacctParser.class.getName());

	/**
	 * Parses the <code>pacct</code> accounting file.  See
	 * <code>include/linux/acct.h</code> for format.
	 * 
	 * @param is               the input stream to parse
	 * @return                 a map of Integer to Integer forks
	 * @throws IOException     if an I/O error occurs
	 */
	public static Map parseLog(InputStream is) throws IOException {
		Map forkMap = new HashMap();
		while (is.available() > 0) {
			// Note: TarInputStream's skip() seems broken, so read() instead
			is.read(); // 
			byte version = (byte)is.read();
			if (version < 3) {
				log.warning("Invalid accounting version: " + version);
				return null;
			}
			for (int i=0; i<14; i++) { // tty, exitcode, uid, gid
				is.read();
			}
			Integer pid = new Integer((int)readUInt32(is));
			Integer ppid = new Integer((int)readUInt32(is));
			List forkedPids = (List)forkMap.get(ppid);
			if (forkedPids == null) {
				forkedPids = new ArrayList();
			}
			forkedPids.add(pid);
			forkMap.put(ppid, forkedPids);
			for (int i=0; i<24; i++) { // times, mem, faults, etc.
				is.read();
			}
			byte[] buff = new byte[16];
			is.read(buff); // comm
			StringBuffer comm = new StringBuffer();
			for (int i=0; i<buff.length; i++) {
				if (buff[i] == 0) {
					break;
				}
				comm.append((char)buff[i]);
			}
			//log.fine(comm + " (" + pid + ") was forked by " + ppid);
		}
		return forkMap;
	}
	
	private static long readUInt32(InputStream is) throws IOException {
		return (is.read() & 0xFF) | ((is.read() & 0xFF) << 8) |
		       ((is.read() & 0xFF) << 16) | ((is.read() & 0xFF) << 24);
	}
	
	/**
	 * Returns a list of all parent PIDs (parent, grandparent, etc.) for the
	 * specified PID.
	 * 
	 * @param pid      the PID to get the PPIDs for
	 * @param forkMap  fork map
	 * @return         a list of parent PIDs
	 */
	public static List getPPIDs(int pid, Map forkMap) {
		if (forkMap == null) {
			return null;
		}
		// find the process that forked the specified PID
		Integer ppid = null;
		Integer iPid = new Integer(pid);
		for (Iterator i = forkMap.entrySet().iterator(); i.hasNext();) {
			Map.Entry entry = (Map.Entry)i.next();
			List pids = (List)entry.getValue();
			if (pids.contains(iPid)) {
				ppid = (Integer)entry.getKey();
				break;
			}
		}
		if (ppid == null) {
			return null;
		} else if (ppid.intValue() != pid) {
			List recPpids = getPPIDs(ppid.intValue(), forkMap);
			List ppids = new ArrayList();
			ppids.add(ppid);
			if (recPpids != null) {
				ppids.addAll(recPpids);
			}
			return ppids;
		} else {
			return null;
		}
	}
} public class ProcDiskStatParser {
	private static final Logger log = Logger.getLogger(ProcDiskStatParser.class.getName());

	private static final String DISK_REGEX = "hd.|sd.";
	
	/** DiskStatSample encapsulates a /proc/diskstat sample. */
	private static class DiskStatSample {
		long[] values  = new long[3]; // {rsect, wsect, use}
		long[] changes = new long[3]; // {rsect, wsect, use}
	}
	
	/**
	 * Parses the <code>proc_diskstats.log</code> file.  The output from
	 * <code>/proc/diskstat</code> is used to collect the disk statistics.
	 * 
	 * @param is      the input stream to read from
	 * @param numCpu  number of processors
	 * @return        disk statistics ({@link DiskUtilSample} and
	 *                {@link DiskTPutSample} samples)
	 * @throws IOException  if an I/O error occurs
	 */
	public static Stats parseLog(InputStream is, int numCpu)
		throws IOException {
		BufferedReader reader = Common.getReader(is);
		String line = reader.readLine();

		int numSamples = 0;
		Stats diskStats = new Stats();
		// last time
		Date ltime = null;
		// a map of /proc/diskstat values for each disk
		Map diskStatMap = new HashMap();
		
		while (line != null) {
			// skip empty lines
			while (line != null && line.trim().length() == 0) {
				line = reader.readLine();
			}
			if (line == null) {
				// EOF
				break;
			}
			line = line.trim();
			if (line.startsWith("#")) {
				continue;
			}
			Date time = null;
			if (line.matches("^\\d+$")) {
				time = new Date(Long.parseLong(line) * 10);
				numSamples++;
			} else {
				line = reader.readLine();
				continue;
			}
			
			// read stats for all disks
			line = reader.readLine();
			while (line != null && line.trim().length() > 0) {
				line = line.trim();
				// {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq}
				String[] tokens = line.split("\\s+");
				if (tokens.length != 14 || !tokens[2].matches(DISK_REGEX)) {
					line = reader.readLine();
					continue;
				}
				String disk = tokens[2];
				
				long rsect = Long.parseLong(tokens[5]);
				long wsect = Long.parseLong(tokens[9]);
				long use = Long.parseLong(tokens[12]);
				DiskStatSample sample = (DiskStatSample)diskStatMap.get(disk);
				if (sample == null) {
					sample = new DiskStatSample();
					diskStatMap.put(disk, sample);
				}
				if (ltime != null) {
					sample.changes[0] = rsect - sample.values[0];
					sample.changes[1] = wsect - sample.values[1];
					sample.changes[2] = use - sample.values[2];
				}
				sample.values = new long[]{rsect, wsect, use};
				line = reader.readLine();
			}
			if (ltime != null) {
				long interval = time.getTime() - ltime.getTime();
				interval = Math.max(interval, 1);
				
				// sum up changes for all disks
				long[] sums = new long[3];
				for (Iterator i=diskStatMap.entrySet().iterator(); i.hasNext(); ) {
					Map.Entry entry = (Map.Entry)i.next();
					DiskStatSample sample = (DiskStatSample)entry.getValue();
					for (int j=0; j<3; j++) {
						sums[j] += sample.changes[j];
					}
				}
				
				// sector size is 512 B
				double readTPut = sums[0] / 2.0 * 1000.0 / interval;
				double writeTPut = sums[1] / 2.0 * 1000.0/ interval;
				// number of ticks (1000/s), reduced to one CPU
				double util = (double)sums[2] / interval / numCpu;
				util = Math.max(0.0, Math.min(1.0, util));
				
				DiskTPutSample tputSample = new DiskTPutSample(time, readTPut, writeTPut);
				DiskUtilSample utilSample = new DiskUtilSample(time, util);
				diskStats.addSample(tputSample);
				diskStats.addSample(utilSample);
			}
			ltime = time;
			if (numSamples > Common.MAX_PARSE_SAMPLES) {
				break;
			}
		}
		log.fine("Parsed " + diskStats.getSamples().size() + " /proc/diskstats samples");
		return diskStats;
	}
	
	/**
	 * Returns the maximum throughput seen in the iostat sample list.
	 * 
	 * @param ioSampleList  iostat sample list
	 * @return              maximum throughput
	 */
	public static double getMaxDiskTPut(List ioSampleList) {
		double maxTPut = 0.0;
		for (Iterator i = ioSampleList.iterator(); i.hasNext();) {
			Sample sample = (Sample)i.next();
			if (sample instanceof DiskTPutSample) {
				DiskTPutSample tputSample = (DiskTPutSample)sample;
				double tput = tputSample.read + tputSample.write;
				if (tput > maxTPut) {
					maxTPut = tput;
				}
			}
		}
		return maxTPut;
	}
} public class ProcNetDevParser {
	private static final Logger log = Logger.getLogger(ProcNetDevParser.class.getName());

	/** NetDevSample encapsulates a /proc/net/dev sample. */
	private static class NetDevSample {
		long[] values  = new long[2]; // {rbytes, wbytes}
		long[] changes = new long[2]; // {rbytes, wbytes}
	}
	
	/**
	 * Parses the <code>proc_netdev.log</code> file.  The output from
	 * <code>/proc/net/dev</code> is used to collect the network interface
	 * statistics, which is returned as disk throughput statistics.
	 * 
	 * @param is      the input stream to read from
	 * @return        disk statistics {@link DiskTPutSample} samples)
	 * @throws IOException  if an I/O error occurs
	 */
	public static Stats parseLog(InputStream is)
		throws IOException {
		BufferedReader reader = Common.getReader(is);
		String line = reader.readLine();

		int numSamples = 0;
		Stats diskStats = new Stats();
		// last time
		Date ltime = null;
		// a map of /proc/diskstat values for each disk
		Map diskStatMap = new HashMap();
		
		while (line != null) {
			// skip empty lines
			while (line != null && line.trim().length() == 0) {
				line = reader.readLine();
			}
			if (line == null) {
				// EOF
				break;
			}
			line = line.trim();
			if (line.startsWith("#")) {
				continue;
			}
			Date time = null;
			if (line.matches("^\\d+$")) {
				time = new Date(Long.parseLong(line) * 10);
				numSamples++;
			} else {
				line = reader.readLine();
				continue;
			}
			
			// eat the header lines
			reader.readLine();
			reader.readLine();
			
			// read stats for all disks
			line = reader.readLine();
			while (line != null && line.trim().length() > 0) {
				line = line.trim();
				// {interface rbytes rpackets rerrs rdrop rfifo rframe rcompressed rmulticast
				// 	          tbytes tpackets terrs tdrop tfifo tframe tcompressed tmulticast }
				String[] tokens = line.split(":");
				String iface = tokens[0];
				if (tokens.length != 2 || "lo".equals(tokens[0])) {
					line = reader.readLine();
					continue;
				}
				tokens = tokens[1].trim().split("\\s+");
				if (tokens.length != 16) {
					line = reader.readLine();
					continue;
				}
				
				long rbytes = Long.parseLong(tokens[1]);
				long tbytes = Long.parseLong(tokens[9]);
				NetDevSample sample = (NetDevSample)diskStatMap.get(iface);
				if (sample == null) {
					sample = new NetDevSample();
					diskStatMap.put(iface, sample);
				}
				if (ltime != null) {
					sample.changes[0] = rbytes - sample.values[0];
					sample.changes[1] = tbytes - sample.values[1];
				}
				sample.values = new long[]{rbytes, tbytes};
				line = reader.readLine();
			}
			if (ltime != null) {
				long interval = time.getTime() - ltime.getTime();
				interval = Math.max(interval, 1);
				
				// sum up changes for all disks
				long[] sums = new long[2];
				for (Iterator i=diskStatMap.entrySet().iterator(); i.hasNext(); ) {
					Map.Entry entry = (Map.Entry)i.next();
					NetDevSample sample = (NetDevSample)entry.getValue();
					for (int j=0; j<2; j++) {
						sums[j] += sample.changes[j];
					}
				}
				
				double readTPut = sums[0] * 1000.0 / interval;
				double writeTPut = sums[1] * 1000.0 / interval;
				
				NetDevTPutSample tputSample = new NetDevTPutSample(time, readTPut, writeTPut);
				diskStats.addSample(tputSample);
			}
			ltime = time;
			if (numSamples > Common.MAX_PARSE_SAMPLES) {
				break;
			}
		}
		log.fine("Parsed " + diskStats.getSamples().size() + " /proc/net/dev samples");
		return diskStats;
	}
} public class ProcPsParser {
	private static final Logger log = Logger.getLogger(ProcPsParser.class.getName());
	
	/**
	 * Maximum time difference between two consecutive samples.  Anything more
	 * indicates an error.
	 */
	private static final int MAX_SAMPLE_DIFF = 60000;
	/**
	 * Maximum uptime for a sample.  Used to sanity check log file values and
	 * ignore inconsistent samples.
	 */
	private static final long MAX_UPTIME =
	  System.currentTimeMillis() - 1072911600000L; // 30 years+ uptime

	/**
	 * Parses the <code>proc_ps.log</code> file.  The output from
	 * <code>/proc/[PID]stat</code> is used to collect process 
	 * information.
	 * 
	 * <p>If <code>pidNameMap</code> is set, it is used to map PIDs to
	 * command names.  This is useful when init scripts are sourced, and thus
	 * ps is unable to report the proper process name.  A sysinit
	 * modification is necessary to generate the mapping log file.</p>
	 * <p><code>forkMap</code> is an optional map that provides detailed
	 * information about process forking.<p>
	 * 
	 * @param is               the input stream to read from
	 * @param pidNameMap       PID to name mapping map (optional)
	 * @param forkMap          process forking map (optional)
	 * @return                 process statistics
	 * @throws IOException     if an I/O error occurs
	 */
	public static PsStats parseLog(InputStream is, Map pidNameMap, Map forkMap)
		throws IOException {
		BufferedReader reader = Common.getReader(is);
		String line = reader.readLine();

		Map processMap = new TreeMap();
		Map lastUserCpuTimes = new HashMap();
		Map lastSysCpuTimes = new HashMap();
		int numSamples = 0;
		
		Date startTime = null;
		Date time = null;
		// last time
		Date ltime = null;
		long lastUptime = -1;
		while (line != null) {
			// skip empty lines
			while (line != null && line.trim().length() == 0) {
				line = reader.readLine();
			}
			if (line == null) {
				// EOF
				break;
			}
			line = line.trim();
			if (line.matches("^\\d+$")) {
				// jiffies (1/100s uptime)
				long uptime = Long.parseLong(line) * 10;
				if (lastUptime != -1 &&
					(uptime < lastUptime || uptime - lastUptime > MAX_SAMPLE_DIFF)) {
					log.warning("Invalid uptime: " + uptime + ", ignoring sample");
					line = reader.readLine();
					continue;
				} else if (uptime > MAX_UPTIME) {
					log.warning("Invalid uptime: " + uptime + ", ignoring sample");
					line = reader.readLine();
					continue;
				}
				lastUptime = uptime;
				time = new Date(uptime);
				
				if (startTime == null) {
					startTime = time;
				}
				numSamples++;
			} else {
				line = reader.readLine();
				continue;
			}
			line = reader.readLine();
			while (line != null && line.trim().length() > 0) {
				line = line.trim();
				/*
				 * See proc(5) for details.
				 * 
				 * {pid, comm, state, ppid, pgrp, session, tty_nr, tpgid,
				 *  flags, minflt, cminflt, majflt, cmajflt, utime, stime,
				 *  cutime, cstime, priority, nice, 0, itrealvalue, starttime, 
				 *  vsize, rss, rlim, startcode, endcode, startstack, 
				 *  kstkesp, kstkeip}
				 */
				int pos = line.indexOf(' ');
				if (pos == -1) {
					log.fine("Invalid line: " + line);
					line = reader.readLine();
					continue;
				}
				int pid = Integer.parseInt(line.substring(0, line.indexOf(' ')));
				int p1 = line.indexOf('(') + 1;
				int p2 = line.lastIndexOf(')');
				if (p1 == -1 || p2 == -1) {
					log.fine("Invalid line: " + line);
					line = reader.readLine();
					continue;
				}
				String cmd = line.substring(p1, p2);
				
				if (pid == 0) {
					log.fine("Invalid line: " + line);
					line = reader.readLine();
					continue;
				}
				// Note that indexes get shifted by -2.
				String[] data = line.substring(p2 + 1).trim().split("\\s+");
				
				Process proc = (Process)processMap.get(new Integer(pid));
				if (proc == null) {
					int ppid = Integer.parseInt(data[1]);
					proc = new Process(pid, cmd);
					proc.ppid = ppid;
					proc.startTime = new Date(Math.min(Integer.parseInt(data[19]) * 10, time.getTime()));
					processMap.put(new Integer(pid), proc);
				} else {
					proc.cmd = cmd;
				}
				
				int state = getState(data[0]);
				long userCpu = Long.parseLong(data[11]);
				long sysCpu = Long.parseLong(data[12]);
				double cpuLoad = 0.0;
				Long lUserCpu = (Long)lastUserCpuTimes.get(new Integer(pid));
				Long lSysCpu = (Long)lastSysCpuTimes.get(new Integer(pid));
				if (lUserCpu != null && lSysCpu != null && ltime != null) {
					long interval = time.getTime() - ltime.getTime();
					//interval = Math.max(interval, 1);
					double userCpuLoad =
						(double)(userCpu - lUserCpu.longValue()) * 10.0 / interval;
					double sysCpuLoad =
						(double)(sysCpu - lSysCpu.longValue()) * 10.0 / interval;
					cpuLoad = userCpuLoad + sysCpuLoad;
					// normalize
					if (cpuLoad > 1.0) {
						userCpuLoad /= cpuLoad;
						sysCpuLoad /= cpuLoad;
					}
					CPUSample procCpuSample = new CPUSample(null, userCpuLoad, sysCpuLoad, 0.0);
					ProcessSample procSample =
						new ProcessSample(time, state, procCpuSample, null, null);
					proc.samples.add(procSample);
				}
				lastUserCpuTimes.put(new Integer(pid), new Long(userCpu));
				lastSysCpuTimes.put(new Integer(pid), new Long(sysCpu));
				
				if (cpuLoad > 0.0) {
					state = Process.STATE_RUNNING;
				}
				line = reader.readLine();
			}
			ltime = time;
			if (numSamples > Common.MAX_PARSE_SAMPLES) {
				break;
			}
		}

		log.fine("Parsed " + numSamples + " process samples "
			     + "(" + processMap.values().size() + " processes)");
		
		// set process parents
		for (Iterator i=processMap.values().iterator(); i.hasNext(); ) {
			Process p = (Process)i.next();
			if (forkMap != null) {
				// check if the forkMap contains the PPID
				List ppids = PacctParser.getPPIDs(p.pid, forkMap);
				if (ppids != null) {
					for (Iterator j=ppids.iterator(); j.hasNext(); ) {
						int ppid = ((Integer)j.next()).intValue();
						if (processMap.get(new Integer(ppid)) != null) {
							p.ppid = ppid;
							break;
						}
					}
				}
			}
			if (p.ppid != -1) {
				p.parent = (Process)processMap.get(new Integer(p.ppid));
				if (p.parent == null && p.pid > 1) {
					log.fine("No parent for: " + p);
				}
			}
		}
		
		int samplePeriod = (int)(time.getTime() - startTime.getTime()) / numSamples;
		log.fine("Sample period: " + samplePeriod);
		
		// update process times, names and descriptions
		for (Iterator i=processMap.values().iterator(); i.hasNext(); ) {
			Process p = (Process)i.next();
			if (p.samples.size() > 0) {
			  Sample fs = (Sample)p.samples.get(0);
			  p.startTime = new Date(Math.min(fs.time.getTime(), p.startTime.getTime()));
			  Sample ls = (Sample)p.samples.get(p.samples.size() - 1);
			  p.duration = ls.time.getTime() - p.startTime.getTime() + samplePeriod;
            } else {
              p.duration = 0;
            }
			
			/*
			 * Check if the pid-name map provides detailed information about
			 * the process.
			 */
			if (pidNameMap != null &&
				pidNameMap.containsKey(new Integer(p.pid))) {
				String[] cmdDesc = (String[])pidNameMap.get(new Integer(p.pid));
				p.cmd = cmdDesc[0];
				StringBuffer desc = new StringBuffer();
				if (cmdDesc.length > 1) {
					desc.append(cmdDesc[1]);
					for (int j=2; j<cmdDesc.length; j++) {
						desc.append("\n" + cmdDesc[j]);
					}
				}
				p.desc = desc.toString();
			}
			
			int activeSamples = 0;
			for (Iterator j=p.samples.iterator(); j.hasNext(); ) {
				ProcessSample sample = (ProcessSample)j.next();
				if (sample.cpu != null &&
					sample.cpu.user + sample.cpu.sys + sample.cpu.io > 0.0) {
					activeSamples++;
				} else if (sample.state == Process.STATE_WAITING) {
					activeSamples++;
				}
			}
			p.active = (activeSamples > 2);
		}
		PsStats psStats = new PsStats();
		psStats.processList = new ArrayList(processMap.values());
		psStats.samplePeriod = samplePeriod;
		psStats.startTime = startTime;
		psStats.endTime = time;
		return psStats;
	}
	
	/**
     * Returns the process state.  State can be one of:
     * <ul>
     *   <li>"D": uninterruptible sleep</li>
     *   <li>"R": running</li>
     *   <li>"S": sleeping</li>
     *   <li>"T": traced or stopped</li>
     *   <li>"Z": zombie</li>
     * </ul>
     * 
     * @param state  process state string
     * @return       process state
     */
    public static int getState(String state) {
        if ("D".equals(state)) {
        	return Process.STATE_WAITING;
        } else if ("R".equals(state)) {
        	return Process.STATE_RUNNING;
        } else if ("S".equals(state)) {
        	return Process.STATE_SLEEPING;
        } else if ("T".equals(state)) {
        	return Process.STATE_STOPPED;
        } else if ("Z".equals(state)) {
        	return Process.STATE_ZOMBIE;
        } else {
        	return Process.STATE_UNDEFINED;
        }
    }
    
} public class EPSRenderer extends ImageRenderer {
	private static final Logger log = Logger.getLogger(EPSRenderer.class.getName());
	
	/** Whether to compress EPS output. */
	public static final boolean COMPRESS_EPS = true;
	
	/*
	 * inherit javadoc
	 */
	public void render(Properties headers, BootStats bootStats, OutputStream os)
		throws IOException {
		g = new EpsGraphics2D();
		allowAlpha = false;
	 	super.render(headers, bootStats, null);
		log.fine("Writing image");
		
		if (COMPRESS_EPS) {
			GZIPOutputStream gzOs = new GZIPOutputStream(os);
			gzOs.write(g.toString().getBytes());
			gzOs.close();
		} else {
			os.write(g.toString().getBytes());
		}
	}
	
	/*
	 * inherit javadoc
	 */
	public String getFileSuffix() {
		return COMPRESS_EPS ? "eps.gz" : "eps";
	}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.compress.tar.TarEntry;
import org.apache.commons.compress.tar.TarInputStream;
import org.bootchart.common.BootStats;
import org.bootchart.common.Common;
import org.bootchart.common.ProcessTree;
import org.bootchart.common.PsStats;
import org.bootchart.common.Stats;
import org.bootchart.parser.HeaderParser;
import org.bootchart.parser.linux.PacctParser;
import org.bootchart.parser.linux.PidNameParser;
import org.bootchart.parser.linux.ProcDiskStatParser;
import org.bootchart.parser.linux.ProcNetDevParser;
import org.bootchart.parser.linux.ProcPsParser;
import org.bootchart.parser.linux.ProcStatParser;
import org.bootchart.parser.linux.PsParser;
import org.bootchart.renderer.EPSRenderer;
import org.bootchart.renderer.PNGRenderer;
import org.bootchart.renderer.Renderer;
import org.bootchart.renderer.SVGRenderer;
/*
 * Bootchart -- Boot Process Visualization
 *
 * Copyright (C) 2004  Ziga Mahkovec <ziga.mahkovec@klika.si>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.bootchart;




/**
 * Bootchart main class.
 */
public class Main {
	private static final Logger log = Logger.getLogger(Main.class.getName());
	
	/** Default image format. */
	private static final String DEFAULT_FORMAT =
		Common.isPNGSupported() ? "png" : "svg";

	/**
	 * Main.
	 * 
	 * @param args        command arguments
	 * @throws Exception  if an error occurs
	 */
	public static void main(String[] args) throws Exception {
		long time = System.currentTimeMillis();

		Options options = getOptions();
		CommandLineParser parser = new GnuParser();
		CommandLine cmd = null;