package edu.cmu.cs.ark.yc.config;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Iterator;
import com.martiansoftware.jsap.CommandLineTokenizer;
import com.martiansoftware.jsap.FlaggedOption;
import com.martiansoftware.jsap.JSAP;
import com.martiansoftware.jsap.JSAPException;
import com.martiansoftware.jsap.JSAPResult;
import com.martiansoftware.jsap.StringParser;
import com.martiansoftware.jsap.Switch;
import com.martiansoftware.jsap.stringparsers.FileStringParser;
import edu.cmu.cs.ark.yc.config.stringparsers.KeyValueParser;
import edu.cmu.cs.ark.yc.config.stringparsers.PositiveDoubleStringParser;
import edu.cmu.cs.ark.yc.config.stringparsers.PositiveIntegerStringParser;
/**
* This is an extension of <a href="http://martiansoftware.com/jsap/">Java Simple Argument Parser (JSAP)</a> that allows the use of a default <i>"configuration file"</i> using the <code>--config-file</code> option.
* <p>
* Through the <code>--config-file</code> option, {@link AppConfig} will append the contents of the configuration file to the given arguments.
* <p>
* A configuration file is essentially a text file with the command line arguments. These arguments can be separated across multiple lines and there can be comments (lines prefixed with the <code>#</code> character).
* <p>
* An example configuration file: <blockquote> <code># set the lambdas<br>
* --lambda 5,6,7<br>
* # verbose settings<br>
* --verbose</code> </blockquote>
*
* @see JSAP
* @author Yanchuan Sim
* @version 0.2
*/
public class AppConfig extends JSAP
{
/**
* {@link StringParser} for positive integers.
*/
public static final PositiveIntegerStringParser POSITIVE_INTEGER_PARSER = new PositiveIntegerStringParser();
/**
* {@link StringParser} for positivedoubles.
*/
public static final PositiveDoubleStringParser POSITIVE_DOUBLE_PARSER = new PositiveDoubleStringParser();
/**
* {@link StringParser} for files.
*/
public static final FileStringParser FILE_PARSER = FileStringParser.getParser();
/**
* {@link StringParser} for key-value pairs.
*/
public static final KeyValueParser KEYVALUE_PARSER = new KeyValueParser();
/**
* Name of this application
*/
private final String name;
/**
* Description string for this application
*/
private final String description;
/**
* Whether to enable the <code>--config-file</code> option
*/
private final boolean enable_config;
private boolean exit_after_help;
/**
* Creates a new application configuration whose name is <code>name</code> and a short description about the application.
* <p>
* The <code>-h/--help</code>, <code>-c/--config-file</code> options are enabled and the program exits after displaying usage information (see {@link #AppConfig(String, String, boolean, boolean, boolean)}).
*
* @param name
* name of application
* @param description
* short description of app.
*/
public AppConfig(String name, String description)
{
super();
this.name = name;
this.description = description;
this.enable_config = true;
this.exit_after_help = true;
init(true);
}
/**
* Creates a new application configuration whose name is <code>name</code> and a short description about the application.
* <p>
* The <code>-h/--help</code> is enabled and the program exits after displaying usage information (see {@link #AppConfig(String, String, boolean, boolean, boolean)}).
*
* @param name
* name of application
* @param description
* short description of app.
* @param enableConfigFile
* enable the use of a <code>--config-file</code> option where default settings are loaded from
*/
public AppConfig(String name, String description, boolean enableConfigFile)
{
super();
this.name = name;
this.description = description;
this.enable_config = enableConfigFile;
this.exit_after_help = true;
init(true);
}
/**
* Creates a new application configuration whose name is <code>name</code> and a short description about the application.
* <p>
* The program exits after displaying usage information (see {@link #AppConfig(String, String, boolean, boolean, boolean)}).
*
* @param name
* name of application
* @param description
* short description of app.
* @param enableConfigFile
* enable the use of a -c/--config-file option where default settings are loaded from
* @param addHelpArgument
* enable the use of a -h/--help flag
*/
public AppConfig(String name, String description, boolean enableConfigFile, boolean addHelpArgument)
{
super();
this.name = name;
this.description = description;
this.enable_config = enableConfigFile;
this.exit_after_help = true;
init(addHelpArgument);
}
/**
* Creates a new application configuration whose name is <code>name</code> and a short description about the application.
*
* @param name
* name of application
* @param description
* short description of app.
* @param enableConfigFile
* enable the use of a -c/--config-file option where default settings are loaded from
* @param addHelpArgument
* enable the use of a -h/--help flag
* @param exitAfterHelp
* whether to exit after displaying usage information when user chooses <code>-h/--help</code>
*/
public AppConfig(String name, String description, boolean enableConfigFile, boolean addHelpArgument, boolean exitAfterHelp)
{
super();
this.name = name;
this.description = description;
this.enable_config = enableConfigFile;
this.exit_after_help = exitAfterHelp;
init(addHelpArgument);
}
private void init(boolean addHelpArgument)
{
try
{
if (addHelpArgument)
this.registerParameter(new Switch("help", 'h', "help", "Displays this usage information."));
if (enable_config)
this.registerParameter(new FlaggedOption("config-file", FileStringParser.getParser().setMustExist(true).setMustBeFile(true), null, false, JSAP.NO_SHORTFLAG, "config-file", "Load configutation options from here."));
}
catch (JSAPException e)
{
e.printStackTrace();
System.exit(-1);
}
}
/**
* Tokenizes <code>cmdLine</code> and passes it to {@link #parse(String[])}.
*
* @param cmdLine
* An array of command line arguments to parse.
*/
@Override
public JSAPResult parse(String cmdLine)
{
return parse(CommandLineTokenizer.tokenize(cmdLine));
}
/**
* Overloads {@link JSAP#parse(String[])} by providing help messages, and more importantly, checking and loading configuration settings from file automatically.
*
* @param args
* An array of command line arguments to parse.
*/
@Override
public JSAPResult parse(String[] args)
{
JSAPResult result = super.parse(args);
if (result.contains("help"))
{
displayUsage();
if (exit_after_help)
System.exit(0);
return result;
}
// if we find a config-file being specified, read it and append it to our
// arguments!
if (enable_config && result.contains("config-file"))
{
try
{
String[] config_commands = CommandLineTokenizer.tokenize(getArgumentsFromFile(result.getFile("config-file")));
String[] combined_commands = new String[args.length + config_commands.length];
for (int i = 0; i < args.length; i++)
combined_commands[i] = args[i];
for (int i = 0; i < config_commands.length; i++)
combined_commands[args.length + i] = config_commands[i];
result = super.parse(combined_commands);
}
catch (IOException e)
{
result.addException("config-file", e);
}
}
if (result.contains("help") && result.getBoolean("help"))
{
displayUsage();
if (exit_after_help)
System.exit(0);
}
return result;
}
/**
* Displays errors that occurred during parsing of command line argument.
*
* @param result
* display error messages from this {@link JSAPResult} object.
*/
public void displayHelp(JSAPResult result)
{
for (@SuppressWarnings("unchecked")
Iterator<String> iter = result.getBadParameterIDIterator(); iter.hasNext();)
{
String id = iter.next();
for (Exception e : result.getExceptionArray(id))
System.err.format("%s: %s\n", id == null ? name : id, e.getMessage());
}
System.err.println();
System.err.println("Use argument -h/--help to display detailed usage information.");
}
/**
* Displays usage information for this application.
*/
public void displayUsage()
{
System.err.format("Usage: %s %s\n", name, this.getUsage());
System.err.println(description);
System.err.println();
System.err.println("Available options are:");
System.err.println(this.getHelp());
}
/**
* Concatenates multiple non commented lines from a text file, i.e get command line from a multiline text file.
* <p>
* <blockquote><b>Note</b>: Assumes utf-8 encoding.</blockquote>
*
* @param config_file
* a {@link File} object pointing to the user selected configuration file.
* @return non-commented strings concatenated together.
* @throws IOException
* when there is an I/O error reading the file.
*/
public static String getArgumentsFromFile(File config_file) throws IOException
{
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(config_file), "utf-8"));
String line;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null)
{
line = line.trim();
if (line.startsWith("#") || line.isEmpty())
continue;
sb.append(line);
sb.append(' ');
}
br.close();
return sb.toString();
}
}