/*
* This file is part of JOP, the Java Optimized Processor
* see <http://www.jopdesign.com/>
*
* Copyright (C) 2008, Benedikt Huber (benedikt.huber@gmail.com)
* Copyright (C) 2010, Stefan Hepp (stefan@stefant.org).
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package com.jopdesign.common.config;
import com.jopdesign.common.logger.LogConfig;
import org.apache.log4j.Logger;
import java.util.HashSet;
import java.util.Set;
/**
* Typed options for improved command line interface
*
* @author Benedikt Huber <benedikt.huber@gmail.com>
* @author Stefan Hepp <stefan@stefant.org>
* @param <T> java type of the option
*/
public abstract class Option<T> {
public static final Logger logger = Logger.getLogger(LogConfig.LOG_CONFIG+".Option");
public static final char SHORT_NONE = ' ';
protected String key;
protected char shortKey = SHORT_NONE;
protected String description;
protected boolean optional;
protected boolean replaceOptions;
/**
* If an option with this flag is set, OptionChecker is not executed (for flags like 'help' or 'version').
*/
protected boolean skipChecks = false;
protected Class<T> valClass;
protected T defaultValue = null;
@SuppressWarnings("unchecked")
public Option(String key, String descr, T defaultVal) {
// Class<T> cast is always safe, shortcoming of Java Generics
this(key, (Class<T>) defaultVal.getClass(), descr, true);
this.defaultValue = defaultVal;
replaceOptions = true;
}
protected Option(String key, Class<T> optClass, String descr, boolean optional) {
this.key = key;
this.valClass = optClass;
this.description = descr;
this.optional = optional;
replaceOptions = true;
}
/**
* Get the default value of this option, or null if not set.
*
* @param options the optiongroup of this option used to get values for keyword replacements.
* @return the default value or null if none.
*/
public T getDefaultValue(OptionGroup options) {
// keyword replacement only used for string option, implemented there.
return defaultValue;
}
/**
* Get the default value without keyword replacements.
*
* @return the original default value, or null if not set.
*/
public T getDefaultValue() {
return defaultValue;
}
public String getDescription() {
return description;
}
public String getKey() {
return key;
}
public char getShortKey() {
return shortKey;
}
public boolean isOptional() {
return optional;
}
public boolean doSkipChecks() {
return skipChecks;
}
/**
* Set to true to disable correctness checks of arguments, i.e. for 'help' and 'version' options.
*
* @param skipChecks set skipCheck flag.
* @return a reference to this object for chaining.
*/
public Option<T> setSkipChecks(boolean skipChecks) {
this.skipChecks = skipChecks;
return this;
}
public Option<T> setShortKey(char shortKey) {
this.shortKey = shortKey;
return this;
}
public boolean doReplaceOptions() {
return replaceOptions;
}
/**
* If true, references to other options of the form '<option>' are replaced before parsing.
*
* @param replaceOptions replace placeholder or leave untouched.
* @return a reference to this for chaining.
*/
public Option<T> setReplaceOptions(boolean replaceOptions) {
this.replaceOptions = replaceOptions;
return this;
}
public T parse(OptionGroup options, String s) throws IllegalArgumentException {
if (replaceOptions) {
HashSet<String> stack = new HashSet<String>();
stack.add(options.getConfigKey(this));
return parse(replacePlaceholders(options.getConfig(), s, stack));
} else {
return parse(s);
}
}
protected abstract T parse(String s) throws IllegalArgumentException;
/**
* Check if the given argument is a possible argument value or the next option.
*
* @param arg the argument to check.
* @return true if it should be consumed as value, or false if it should be parsed as the next option.
*/
public boolean isValue(String arg) {
// if it starts with '-', assume it is an option, else a value
// this prevents options being consumed as values.
// specific option types may overwrite this check.
return !arg.startsWith("-");
}
/**
* Check if this option is enabled (i.e. has been set to a value not equal to null or 'false',
* depending on the type of the option).
*
* @param options a reference to the OptionGroup containing this option.
* @return true if this option has been set with a non-zero value.
*/
public boolean isEnabled(OptionGroup options) {
return options.hasValue(this);
}
public String toString() {
return key;
}
public String toString(int lAdjust, OptionGroup options) {
StringBuilder s = new StringBuilder(" ");
if (shortKey != SHORT_NONE) {
s.append('-');
s.append(shortKey);
s.append(",--");
} else {
s.append(" --");
}
String configKey = options.getConfigKey(this);
s.append(configKey);
for (int i = configKey.length(); i < lAdjust; i++) {
s.append(' ');
}
s.append(" ");
s.append(descrString(9+lAdjust, 100-lAdjust, options));
return s.toString();
}
public String descrString(int lAdjust, int cols, OptionGroup options) {
String defaultValue = options.getDefaultValueText(this);
StringBuilder s = new StringBuilder();
for (int i = 0; i < lAdjust; i++) {
s.append(' ');
}
String newline = System.getProperty("line.separator") + s.toString();
String defaults = getDefaultsText(defaultValue);
StringBuilder text = new StringBuilder();
int pos = 0;
while (description.length() - pos > cols) {
int space = description.lastIndexOf(' ', pos+cols);
if (space <= pos) {
space = description.indexOf(' ',pos+cols);
}
if (space == -1) {
text.append(description.substring(pos));
pos = description.length();
} else {
text.append(description.substring(pos,space)).append(newline);
pos = space+1;
}
}
text.append(description.substring(pos));
if (description.length()-pos+defaults.length() > cols) {
text.append(newline).append(defaults);
} else {
text.append(' ').append(defaults);
}
return text.toString();
}
protected String getDefaultsText(String defaultValue) {
StringBuilder s = new StringBuilder();
if (defaultValue != null) {
s.append("[default: ").append(defaultValue).append("]");
} else {
s.append(this.optional ? "[optional]" : "[mandatory]");
}
return s.toString();
}
protected String replacePlaceholders(Config config, String s, Set<String> stack) {
// just some sanity checks..
if (s.contains("$(")) {
// TODO we need to setup a standard logger setup before parsing options so this can be seen before
// LogConfig gets initialized
logger.warn("Found '$(' in value of option "+getKey()+", are you sure you did not mean '${..}' instead?");
}
int p1 = s.indexOf("${");
if (p1 == -1) {
return s;
}
StringBuilder buf = new StringBuilder();
int p2 = -1;
while (p1 > -1) {
buf.append(s.substring(p2 + 1, p1));
p2 = s.indexOf('}', p1);
if (p2 == -1) {
// if no closing } found, stop and add rest including '$['
p2 = p1 - 1;
break;
}
// replace placeholder with value
String key = s.substring(p1 + 2, p2);
// try to use the option if available to get default value from option or environment.
// We do NOT want to use config.getOption(key) here to avoid infinite recursion
Option<?> opt = config.getOptions().getOptionSpec(key);
String val = config.getValue(key, System.getenv(key));
if (val == null && opt != null) {
Object o = opt.getDefaultValue();
val = o != null ? o.toString() : null;
}
if (val == null || stack.contains(key)) {
buf.append("");
} else {
stack.add(key);
buf.append(replacePlaceholders(config, val, stack));
stack.remove(key);
}
p1 = s.indexOf("${", p2);
}
buf.append(s.substring(p2 + 1));
return buf.toString();
}
}