/*
* xtc - The eXTensible Compiler
* Copyright (C) 2005-2007 Robert Grimm
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*/
package xtc.util;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import xtc.parser.ParseError;
import xtc.parser.PParser;
import xtc.parser.Result;
import xtc.parser.SemanticValue;
import xtc.tree.Attribute;
import xtc.tree.Node;
import xtc.tree.Printer;
/**
* A tool's runtime. This helper class processes command line
* options, prints errors and warnings, and manages console output.
*
* @author Robert Grimm
* @version $Revision: 1.27 $
*/
public class Runtime {
/**
* The internal name for the input directory option. The option is
* expected to have multiple directory values.
*/
public static final String INPUT_DIRECTORY = "inputDirectory";
/**
* The internal name for the output directory option. The option is
* expected to have a directory value.
*/
public static final String OUTPUT_DIRECTORY = "outputDirectory";
/**
* The internal name for the intput encoding option. The option is
* expected to have a word value.
*/
public static final String INPUT_ENCODING = "inputEncoding";
/**
* The internal name for the output encoding option. The option is
* expected to have a word value.
*/
public static final String OUTPUT_ENCODING = "outputEncoding";
// ========================================================================
/** The console printer. */
protected Printer console;
/** The error console printer. */
protected Printer errConsole;
/** The list of command line options. */
protected final List<Option> optionList;
/** The map from external names to options. */
protected final Map<String, Option> externalMap;
/** The map from internal names to options. */
protected final Map<String, Option> internalMap;
/** The actual options. */
protected final Map<String, Object> options;
/** The error count. */
protected int errors;
/** The warning count. */
protected int warnings;
// ========================================================================
/**
* Create a new runtime. Note that the list of input directories is
* empty, while the output directory is initialized to the current
* directory.
*/
public Runtime() {
console = new
Printer(new BufferedWriter(new OutputStreamWriter(System.out)));
errConsole = new
Printer(new BufferedWriter(new OutputStreamWriter(System.err)));
optionList = new ArrayList<Option>();
externalMap = new HashMap<String, Option>();
internalMap = new HashMap<String, Option>();
options = new HashMap<String, Object>();
errors = 0;
warnings = 0;
}
// ========================================================================
/**
* Get a printer to the console.
*
* @return A printer to the console.
*/
public Printer console() {
return console;
}
/**
* Update the printer to the console. Since the console is used
* throughout xtc, use this method with caution.
*
* @param console The new console.
*/
public void setConsole(Printer console) {
this.console = console;
}
/**
* Get a printer to the error console.
*
* @return A printer to the error console.
*/
public Printer errConsole() {
return errConsole;
}
/**
* Update the printer to the error console. Since the error console
* is used throughout xtc, use this method with caution.
*
* @param console The new error console.
*/
public void setErrConsole(Printer console) {
errConsole = console;
}
// ========================================================================
/**
* Get an estimate of free memory.
*
* @return An estimate of free memory.
*/
public long freeMemory() {
return java.lang.Runtime.getRuntime().freeMemory();
}
// ========================================================================
/**
* Check that no option with the specified names exits.
*
* @param external The external name.
* @param internal The internal name.
* @throws IllegalArgumentException Signals that an option with
* the external or interal name already exists.
*/
protected void check(String external, String internal) {
if (externalMap.containsKey(external)) {
throw new IllegalArgumentException("Option with external name " +
external + " already exists");
} else if (internalMap.containsKey(internal)) {
throw new IllegalArgumentException("Option with internal name " +
internal + " already exists");
}
}
/**
* Add the specified option. This method adds the specified option
* to the {@link #optionList}, {@link #externalMap}, and {@link
* #internalMap} fields.
*
* @param option The option.
*/
protected void add(Option option) {
optionList.add(option);
externalMap.put(option.external, option);
internalMap.put(option.internal, option);
}
/**
* Declare a boolean command line option.
*
* @param external The external name.
* @param internal The internal name.
* @param value The default value.
* @param description The description.
* @return This runtime.
* @throws IllegalArgumentException Signals that an option with
* the external or interal name already exists.
*/
public Runtime bool(String external, String internal, boolean value,
String description) {
check(external, internal);
add(new Option(Option.Kind.BOOLEAN, external, internal, value, false,
description));
return this;
}
/**
* Declare a word-valued command line option.
*
* @param external The external name.
* @param internal The internal name.
* @param multiple The flag for multiple occurrences.
* @param description The description.
* @throws IllegalArgumentException Signals that an option with
* the external or interal name already exists.
* @return This runtime.
*/
public Runtime word(String external, String internal, boolean multiple,
String description) {
check(external, internal);
add(new Option(Option.Kind.WORD, external, internal, null, multiple,
description));
return this;
}
/**
* Declare an integer-valued command line option.
*
* @param external The external name.
* @param internal The internal name.
* @param value The default value.
* @param description The description.
* @return This runtime.
* @throws IllegalArgumentException Signals that an option with
* the external or interal name already exists.
*/
public Runtime number(String external, String internal, int value,
String description) {
check(external, internal);
add(new Option(Option.Kind.INTEGER, external, internal, new Integer(value),
false, description));
return this;
}
/**
* Declare a file-valued command line option.
*
* @param external The external name.
* @param internal The internal name.
* @param multiple The flag for multiple occurrences.
* @param description The description.
* @return This runtime.
* @throws IllegalArgumentException Signals that an option with
* the external or interal name already exists.
*/
public Runtime file(String external, String internal, boolean multiple,
String description) {
check(external, internal);
add(new Option(Option.Kind.FILE, external, internal, null, multiple,
description));
return this;
}
/**
* Declare a directory-valued command line option. The default
* value is the current directory.
*
* @param external The external name.
* @param internal The internal name.
* @param multiple The flag for multiple occurrences.
* @param description The description.
* @return This runtime.
* @throws IllegalArgumentException Signals that an option with
* the external or interal name already exists.
*/
public Runtime dir(String external, String internal, boolean multiple,
String description) {
check(external, internal);
add(new Option(Option.Kind.DIRECTORY, external, internal,
new File(System.getProperty("user.dir")), multiple,
description));
return this;
}
/**
* Declare an attribute-valued command line option.
*
* @param external The external name.
* @param internal The internal name.
* @param multiple The flag for multiple occurrences.
* @param description The description.
* @throws IllegalArgumentException Signals that an option with
* the external or interal name already exists.
*/
public Runtime att(String external, String internal, boolean multiple,
String description) {
check(external, internal);
add(new Option(Option.Kind.ATTRIBUTE, external, internal, null, multiple,
description));
return this;
}
// ========================================================================
/** Print a description of all command line options to the console. */
public void printOptions() {
// Determine the alignment across all options.
int alignment = 0;
for (Option option : optionList) {
switch (option.kind) {
case BOOLEAN:
alignment = Math.max(alignment, option.external.length() + 5);
break;
case WORD:
case FILE:
alignment = Math.max(alignment, option.external.length() + 5 + 7);
break;
case INTEGER:
case DIRECTORY:
case ATTRIBUTE:
alignment = Math.max(alignment, option.external.length() + 5 + 6);
break;
default:
assert false : "Invalid option " + option;
}
}
// Actually print all options.
for (Option option : optionList) {
console.p(" -").p(option.external);
switch (option.kind) {
case BOOLEAN:
break;
case WORD:
console.p(" <word>");
break;
case INTEGER:
console.p(" <num>");
break;
case FILE:
console.p(" <file>");
break;
case DIRECTORY:
console.p(" <dir>");
break;
case ATTRIBUTE:
console.p(" <att>");
break;
default:
assert false: "Invalid option " + option;
}
console.align(alignment).wrap(alignment, option.description).pln();
}
console.flush();
}
// ========================================================================
/**
* Process the specified command line arguments. This method sets
* all options to their specified values.
*
* @param args The arguments.
* @return The index right after the processed command line options.
*/
public int process(String args[]) {
int index = 0;
options.clear();
while ((index < args.length) && args[index].startsWith("-")) {
if (1 >= args[index].length()) {
error("empty command line option");
} else {
String name = args[index].substring(1);
Option option = externalMap.get(name);
if (null == option) {
error("unrecognized command line option " + name);
} else if ((! option.multiple) &&
(options.containsKey(option.internal))) {
error("repeated " + name + " option");
} else if (Option.Kind.BOOLEAN == option.kind) {
options.put(option.internal, Boolean.TRUE);
} else if (args.length == index + 1) {
error(name + " option without argument");
} else {
Object value = null;
index++;
switch (option.kind) {
case WORD:
value = args[index];
break;
case INTEGER:
try {
value = new Integer(args[index]);
} catch (NumberFormatException x) {
error("malformed integer argument to " + name + " option");
}
break;
case FILE:
File file = new File(args[index]);
if (file.exists()) {
value = file;
} else {
error("nonexistent file argument to " + name + " option");
}
break;
case DIRECTORY:
File dir = new File(args[index]);
if (dir.exists()) {
if (dir.isDirectory()) {
value = dir;
} else {
error(args[index] + " not a directory");
}
} else {
error("nonexistent directory argument to " + name + " option");
}
break;
case ATTRIBUTE:
PParser parser = new PParser(new StringReader(args[index]),
"<console>", args[index].length());
Result result = null;
try {
result = parser.pAttribute(0);
} catch (IOException x) {
error("internal error: " + x);
}
if (! result.hasValue()) {
error("malformed attribute " + args[index] + ": " +
((ParseError)result).msg);
} else if (result.index != args[index].length()) {
error("extra characters after " +
args[index].substring(0, result.index));
} else {
value = ((SemanticValue)result).value;
}
break;
default:
assert false : "Unrecognized option " + option;
}
if (null != value) {
if (option.multiple) {
if (options.containsKey(option.internal)) {
@SuppressWarnings("unchecked")
List<Object> values = (List<Object>)options.get(option.internal);
values.add(value);
} else {
List<Object> values = new ArrayList<Object>();
values.add(value);
options.put(option.internal, values);
}
} else {
options.put(option.internal, value);
}
}
}
}
index++;
}
return index;
}
// ========================================================================
/**
* Initialize all options without values to their defaults. The
* default value for word, file, and attribute options is
* <code>null</code> if no multiple occurrences are allowed and the
* empty list otherwise.
*/
public void initDefaultValues() {
for (Option option : optionList) {
if (! options.containsKey(option.internal)) {
Object value = null;
if (null != option.value) {
if (option.multiple) {
List<Object> list = new ArrayList<Object>(1);
list.add(option.value);
value = list;
} else {
value = option.value;
}
} else if (option.multiple) {
value = new ArrayList<Object>(0);
}
options.put(option.internal, value);
}
}
}
/**
* Initialize all boolean options without values to the specified
* value.
*
* @param value The value.
*/
public void initFlags(boolean value) {
for (Option option : optionList) {
if ((Option.Kind.BOOLEAN == option.kind) &&
(! options.containsKey(option.internal))) {
options.put(option.internal, value);
}
}
}
/**
* Initialize all boolean options with the specified prefix and
* without values to the specified value.
*
* @param prefix The prefix.
* @param value The value.
*/
public void initFlags(String prefix, boolean value) {
for (Option option : optionList) {
if ((Option.Kind.BOOLEAN == option.kind) &&
option.internal.startsWith(prefix) &&
(! options.containsKey(option.internal))) {
options.put(option.internal, value);
}
}
}
/**
* Determine whether the specified option has a value.
*
* @param name The internal name.
* @return <code>true</code> if the option has a value.
*/
public boolean hasValue(String name) {
return options.containsKey(name);
}
/**
* Determine whether any option with the specified prefix has a
* value.
*
* @param prefix The prefix.
* @return <code>true</code> if any option with the prefix has a
* value.
*/
public boolean hasPrefixValue(String prefix) {
for (String s : options.keySet()) {
if (s.startsWith(prefix)) return true;
}
return false;
}
/**
* Get the value of the specified option.
*
* @param name The internal name.
* @return The option's value.
* @throws IllegalArgumentException Signals that the option has no
* value.
*/
public Object getValue(String name) {
if (options.containsKey(name)) {
return options.get(name);
} else {
throw new IllegalArgumentException("Undefined internal option " + name);
}
}
/**
* Test the value of the specified boolean option.
*
* @param name The internal name.
* @return The option's boolean value.
* @throws IllegalArgumentException Signals that the corresponding
* option has no value.
* @throws ClassCastException Signals that the corresponding option
* does not have a boolean value.
*/
public boolean test(String name) {
if (options.containsKey(name)) {
return (Boolean)options.get(name);
} else {
throw new IllegalArgumentException("Undefined boolean option " + name);
}
}
/**
* Get the integer value of the specified option.
*
* @param name The internal name.
* @return The option's integer value.
* @throws IllegalArgumentException Signals that the corresponding
* option has no value.
* @throws ClassCastException Signals that the corresponding option
* does not have an integer value.
*/
public int getInt(String name) {
if (options.containsKey(name)) {
return ((Integer)options.get(name)).intValue();
} else {
throw new IllegalArgumentException("Undefined integer option " + name);
}
}
/**
* Get the string value of the specified option.
*
* @param name The internal name.
* @return The option's string value.
* @throws IllegalArgumentException Signals that the corresponding
* option has no value.
* @throws ClassCastException Signals that the corresponding option
* does not have an integer value.
*/
public String getString(String name) {
if (options.containsKey(name)) {
return (String)options.get(name);
} else {
throw new IllegalArgumentException("Undefined word option " + name);
}
}
/**
* Get the file value of the specified option.
*
* @param name The internal name.
* @return The option's file value.
* @throws IllegalArgumentException Signals that the corresponding
* option has no value.
* @throws ClassCastException Signals that the corresponding option
* does not have a file value.
*/
public File getFile(String name) {
if (options.containsKey(name)) {
return (File)options.get(name);
} else {
throw new IllegalArgumentException("Undefined file/directory option " +
name);
}
}
/**
* Get the list value of the specified option.
*
* @param name The internal name.
* @return The option's list value.
* @throws IllegalArgumentException Signals that the corresponding
* option has no value.
* @throws ClassCastException Signals that the corresponding option
* does not have a list value.
*/
public List<?> getList(String name) {
if (options.containsKey(name)) {
return (List)options.get(name);
} else {
throw new IllegalArgumentException("Undefined option " + name +
" with multiple values");
}
}
/**
* Get the attribute list value of the specified option.
*
* @param name The internal name.
* @return The option's attribute list value.
* @throws IllegalArgumentException Signals that the corresponding
* option has no value.
* @throws ClassCastException Signals that the corresponding option
* does not have an attribute list value.
*/
@SuppressWarnings("unchecked")
public List<Attribute> getAttributeList(String name) {
List<?> l = getList(name);
// Make sure the list actually contains attributes.
if (0 < l.size()) {
@SuppressWarnings("unused")
Attribute a = (Attribute)l.get(0);
}
return (List<Attribute>)l;
}
/**
* Get the file list value of the specified option.
*
* @param name The internal name.
* @return The option's file list value.
* @throws IllegalArgumentException Signals that the corresponding
* option has no value.
* @throws ClassCastException Signals that the corresponding option
* does not have a file list value.
*/
@SuppressWarnings("unchecked")
public List<File> getFileList(String name) {
List<?> l = getList(name);
// Make sure the list actually contains files.
if (0 < l.size()) {
@SuppressWarnings("unused")
File f = (File)l.get(0);
}
return (List<File>)l;
}
/**
* Check that the specified value is valid for the specified option.
*
* @param option The option.
* @param value The value.
* @throws IllegalArgumentException Signals that the value is
* invalid.
*/
protected void check(Option option, Object value) {
switch (option.kind) {
case BOOLEAN:
if (! (value instanceof Boolean)) {
throw new IllegalArgumentException("Invalid value " + value +
" for boolean option " +
option.internal);
}
break;
case WORD:
if (! (value instanceof String)) {
throw new IllegalArgumentException("Invalid value " + value +
" for word option " +
option.internal);
}
break;
case INTEGER:
if (! (value instanceof Integer)) {
throw new IllegalArgumentException("Invalid value " + value +
" for number option " +
option.internal);
}
break;
case FILE:
if ((! (value instanceof File)) ||
(! ((File)value).exists())) {
throw new IllegalArgumentException("Invalid value " + value +
" for file option " +
option.internal);
}
break;
case DIRECTORY:
if ((! (value instanceof File)) ||
(! ((File)value).isDirectory())) {
throw new IllegalArgumentException("Invalid value " + value +
" for directory option " +
option.internal);
}
break;
case ATTRIBUTE:
if (! (value instanceof Attribute)) {
throw new IllegalArgumentException("Invalid value " + value +
" for attribute option " +
option.internal);
}
break;
default:
assert false : "Invalid option " + option;
}
}
/**
* Set the value of the specified option.
*
* @param name The internal name.
* @param value The value.
* @throws IllegalArgumentException Signals an unrecognized option
* or an invalid value.
*/
public void setValue(String name, Object value) {
Option option = internalMap.get(name);
if (null == option) {
throw new IllegalArgumentException("Undefined option " + name);
} else {
check(option, value);
if (option.multiple) {
List<Object> list = new ArrayList<Object>(1);
list.add(value);
value = list;
}
options.put(name, value);
}
}
/**
* Set the value of the specified boolean-valued option.
*
* @param name The internal name.
* @param value The value.
* @throws IllegalArgumentException Signals an unrecognized option
* or not a boolean-valued option.
*/
public void setValue(String name, boolean value) {
Option option = internalMap.get(name);
if (null == option) {
throw new IllegalArgumentException("Undefined option " + name);
} else if (Option.Kind.BOOLEAN != option.kind) {
throw new IllegalArgumentException("Not a boolean-valued option " + name);
} else {
options.put(name, value);
}
}
// ========================================================================
/**
* Locate the specified file. This method searches this runtime's
* list of input directories.
*
* @see #INPUT_DIRECTORY
*
* @param path The (relative) file path.
* @return The corresponding file.
* @throws FileNotFoundException
* Signals that the specified file could not be found.
*/
public File locate(String path) throws FileNotFoundException {
List<File> roots = getFileList(INPUT_DIRECTORY);
if (null != roots) {
for (File root : roots) {
File file = new File(root, path);
if (file.exists() && file.isFile()) {
return file;
}
}
}
throw new FileNotFoundException(path + " not found");
}
/**
* Get a reader for the specified file. The reader uses this
* runtime's input encoding and is buffered.
*
* @see #INPUT_ENCODING
*
* @param file The file.
* @return The corresponding reader.
* @throws IOException Signals an I/O error.
*/
public Reader getReader(File file) throws IOException {
return getReader(new FileInputStream(file));
}
/**
* Get a reader for the specified input stream. The reader uses
* this runtime's input encoding and is buffered.
*
* @see #INPUT_ENCODING
*
* @param in The input stream.
* @return The corresponding reader.
* @throws UnsupportedEncodingException
* Signals that this runtime's encoding is not valid.
*/
public Reader getReader(InputStream in) throws UnsupportedEncodingException {
String encoding = (String)options.get(INPUT_ENCODING);
if (null == encoding) {
return new BufferedReader(new InputStreamReader(in));
} else {
return new BufferedReader(new InputStreamReader(in, encoding));
}
}
/**
* Get this runtime's output directory.
*
* @see #OUTPUT_DIRECTORY
*
* @return The output directory.
*/
public File getOutputDirectory() {
return getFile(OUTPUT_DIRECTORY);
}
/**
* Get a writer for the specified file. The writer uses this
* runtime's output encoding and is buffered.
*
* @see #OUTPUT_ENCODING
*
* @param file The file.
* @return The corresponding writer.
* @throws IOException Signals an I/O error.
*/
public Writer getWriter(File file) throws IOException {
return getWriter(new FileOutputStream(file));
}
/**
* Get a writer for the specified output stream. The writer uses
* this runtime's output encoding and is buffered.
*
* @see #OUTPUT_ENCODING
*
* @param in The output stream.
* @return The corresponding writer.
* @throws UnsupportedEncodingException
* Signals that this runtime's encoding is not valid.
*/
public Writer getWriter(OutputStream in) throws UnsupportedEncodingException {
String encoding = (String)options.get(OUTPUT_ENCODING);
if (null == encoding) {
return new BufferedWriter(new OutputStreamWriter(in));
} else {
return new BufferedWriter(new OutputStreamWriter(in, encoding));
}
}
// ========================================================================
/**
* Determine whether errors have been reported.
*
* @return <code>true</code> if errors have been reported.
*/
public boolean seenError() {
return (0 < errors);
}
/**
* Get the current error count.
*
* @return The current error count.
*/
public int errorCount() {
return errors;
}
/** Record an error reported through another means. */
public void error() {
errors++;
}
/**
* Print the specified error message.
*
* @param msg The error message.
*/
public void error(String msg) {
errConsole.p("error: ").pln(msg).flush();
errors++;
}
/**
* Print the specified error message.
*
* @param msg The error message.
* @param n The offending node.
*/
public void error(String msg, Node n) {
errConsole.loc(n).p(": ");
error(msg);
}
/** Record a warning reported through another means. */
public void warning() {
warnings++;
}
/**
* Print the specified warning message.
*
* @param msg The warning message.
*/
public void warning(String msg) {
errConsole.p("warning: ").pln(msg).flush();
warnings++;
}
/**
* Print the specified warning message.
*
* @param msg The warning message.
* @param n The offending node.
*/
public void warning(String msg, Node n) {
errConsole.loc(n).p(": ");
warning(msg);
}
// ========================================================================
/**
* Exit the tool. This method terminates the Java virtual machine
* with the appropriate exit code and a summary of error and warning
* numbers if any have been reported.
*/
public void exit() {
if (0 < errors) {
if (1 == errors) {
errConsole.p("1 error");
} else {
errConsole.p(errors);
errConsole.p(" errors");
}
}
if (0 < warnings) {
if (0 < errors) {
errConsole.p(", ");
}
if (1 == warnings) {
errConsole.p("1 warning");
} else {
errConsole.p(warnings);
errConsole.p(" warnings");
}
}
if ((0 < errors) || (0 < warnings)) {
errConsole.pln().flush();
}
if (0 < errors) {
System.exit(1);
} else {
System.exit(0);
}
}
}