// NAnt - A .NET build tool
// Copyright (C) 2001 Gerry Shaw
//
// 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
//
// Gert Driesen (gert.driesen@ardatis.com)
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Security.Permissions;
using System.Text.RegularExpressions;
namespace NAnt.Core.Util {
///
/// Represents a valid command-line argument.
///
public class CommandLineArgument {
#region Public Instance Constructors
public CommandLineArgument(CommandLineArgumentAttribute attribute, PropertyInfo propertyInfo) {
_attribute = attribute;
_propertyInfo = propertyInfo;
_seenValue = false;
_elementType = GetElementType(propertyInfo);
_argumentType = GetArgumentType(attribute, propertyInfo);
if (IsCollection || IsArray) {
_collectionValues = new ArrayList();
} else if (IsNameValueCollection) {
_valuePairs = new NameValueCollection();
}
Debug.Assert(LongName != null && LongName.Length > 0);
Debug.Assert((!IsCollection && !IsArray && !IsNameValueCollection) || AllowMultiple, "Collection and array arguments must have allow multiple");
Debug.Assert(!Unique || (IsCollection || IsArray || IsNameValueCollection), "Unique only applicable to collection arguments");
}
#endregion Public Instance Constructors
#region Public Instance Properties
///
/// Gets the property that backs the argument.
///
///
/// The property that backs the arguments.
///
public PropertyInfo Property {
get { return _propertyInfo; }
}
///
/// Gets the underlying of the argument.
///
///
/// The underlying of the argument.
///
///
/// If the of the argument is a collection type,
/// this property will returns the underlying type of that collection.
///
public Type ValueType {
get { return IsCollection || IsArray ? _elementType : Type; }
}
///
/// Gets the long name of the argument.
///
/// The long name of the argument.
public string LongName {
get {
if (_attribute != null && _attribute.Name != null) {
return _attribute.Name;
} else {
return _propertyInfo.Name;
}
}
}
///
/// Gets the short name of the argument.
///
/// The short name of the argument.
public string ShortName {
get {
if (_attribute != null) {
return _attribute.ShortName;
} else {
return null;
}
}
}
///
/// Gets the description of the argument.
///
/// The description of the argument.
public string Description {
get {
if (_attribute != null) {
return _attribute.Description;
} else {
return null;
}
}
}
///
/// Gets a value indicating whether the argument is required.
///
///
/// if the argument is required; otherwise,
/// .
///
public bool IsRequired {
get { return 0 != (_argumentType & CommandLineArgumentTypes.Required); }
}
///
/// Gets a value indicating whether a mathing command-line argument
/// was already found.
///
///
/// if a matching command-line argument was
/// already found; otherwise, .
///
public bool SeenValue {
get { return _seenValue; }
}
///
/// Gets a value indicating whether the argument can be specified multiple
/// times.
///
///
/// if the argument may be specified multiple
/// times; otherwise, .
///
public bool AllowMultiple {
get {
return (IsCollection || IsArray || IsNameValueCollection)
&& (0 != (_argumentType & CommandLineArgumentTypes.Multiple));
}
}
///
/// Gets a value indicating whether the argument can only be specified once
/// with a certain value.
///
///
/// if the argument should always have a unique
/// value; otherwise, .
///
public bool Unique {
get { return 0 != (_argumentType & CommandLineArgumentTypes.Unique); }
}
///
/// Gets the of the property to which the argument
/// is applied.
///
///
/// The of the property to which the argument is
/// applied.
///
public Type Type {
get { return _propertyInfo.PropertyType; }
}
///
/// Gets a value indicating whether the argument is collection-based.
///
///
/// if the argument is backed by a
/// that can be assigned to and is not backed
/// by a that can be assigned to
/// ; otherwise, .
///
public bool IsCollection {
get { return IsCollectionType(Type); }
}
///
/// Gets a value indicating whether the argument is a set of name/value
/// pairs.
///
///
/// if the argument is backed by a
/// that can be assigned to ; otherwise,
/// .
///
public bool IsNameValueCollection {
get { return IsNameValueCollectionType(Type); }
}
///
/// Gets a value indicating whether the argument is array-based.
///
///
/// if the argument is backed by an array;
/// otherwise, .
///
public bool IsArray {
get { return IsArrayType(Type); }
}
///
/// Gets a value indicating whether the argument is the default argument.
///
///
/// if the argument is the default argument;
/// otherwise, .
///
public bool IsDefault {
get { return (_attribute != null && _attribute is DefaultCommandLineArgumentAttribute); }
}
///
/// Gets a value indicating whether the argument cannot be combined with
/// other arguments.
///
///
/// if the argument cannot be combined with other
/// arguments; otherwise, .
///
public bool IsExclusive {
get { return 0 != (_argumentType & CommandLineArgumentTypes.Exclusive); }
}
#endregion Public Instance Properties
#region Public Instance Methods
///
/// Sets the value of the argument on the specified object.
///
/// The object on which the value of the argument should be set.
/// The argument is required and no value was specified.
///
///
/// The matching property is collection-based, but is not initialized
/// and cannot be written to.
///
/// -or-
///
/// The matching property is collection-based, but has no strongly-typed
/// Add method.
///
/// -or-
///
/// The matching property is collection-based, but the signature of the
/// Add method is not supported.
///
///
[ReflectionPermission(SecurityAction.Demand, Flags=ReflectionPermissionFlag.NoFlags)]
public void Finish(object destination) {
if (IsRequired && !SeenValue) {
throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture, "Missing required argument '-{0}'.", LongName));
}
if (IsArray) {
_propertyInfo.SetValue(destination, _collectionValues.ToArray(_elementType), BindingFlags.Default, null, null, CultureInfo.InvariantCulture);
} else if (IsCollection) {
// If value of property is null, create new instance of collection
if (_propertyInfo.GetValue(destination, BindingFlags.Default, null, null, CultureInfo.InvariantCulture) == null) {
if (!_propertyInfo.CanWrite) {
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1171")
+ " but is not initialized and does not allow the"
+ "collection to be initialized.", LongName));
}
object instance = Activator.CreateInstance(_propertyInfo.PropertyType, BindingFlags.Public | BindingFlags.Instance, null, null, CultureInfo.InvariantCulture);
_propertyInfo.SetValue(destination, instance, BindingFlags.Default, null, null, CultureInfo.InvariantCulture);
}
object value = _propertyInfo.GetValue(destination,
BindingFlags.Default, null, null, CultureInfo.InvariantCulture);
MethodInfo addMethod = null;
// Locate Add method with 1 parameter
foreach (MethodInfo method in value.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) {
if (method.Name == "Add" && method.GetParameters().Length == 1) {
ParameterInfo parameter = method.GetParameters()[0];
if (parameter.ParameterType != typeof(object)) {
addMethod = method;
break;
}
}
}
if (addMethod == null) {
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1169"), LongName));
} else {
try {
foreach (object item in _collectionValues) {
addMethod.Invoke(value, BindingFlags.Default, null, new object[] {item}, CultureInfo.InvariantCulture);
}
} catch (Exception ex) {
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1173"),
LongName), ex);
}
}
} else if (IsNameValueCollection) {
// If value of property is null, create new instance of collection
if (_propertyInfo.GetValue(destination, BindingFlags.Default, null, null, CultureInfo.InvariantCulture) == null) {
if (!_propertyInfo.CanWrite) {
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1171")
+ " but is not initialized and does not allow the"
+ "collection to be initialized.", LongName));
}
object instance = Activator.CreateInstance(_propertyInfo.PropertyType, BindingFlags.Public | BindingFlags.Instance, null, null, CultureInfo.InvariantCulture);
_propertyInfo.SetValue(destination, instance, BindingFlags.Default, null, null, CultureInfo.InvariantCulture);
}
object value = _propertyInfo.GetValue(destination,
BindingFlags.Default, null, null, CultureInfo.InvariantCulture);
MethodInfo addMethod = null;
// Locate Add method with 2 string parameters
foreach (MethodInfo method in value.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) {
if (method.Name == "Add" && method.GetParameters().Length == 2) {
if (method.GetParameters()[0].ParameterType == typeof(string) &&
method.GetParameters()[1].ParameterType == typeof(string)) {
addMethod = method;
break;
}
}
}
if (addMethod == null) {
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1169"), LongName));
} else {
try {
foreach (string key in _valuePairs) {
addMethod.Invoke(value, BindingFlags.Default, null,
new object[] {key, _valuePairs.Get(key)},
CultureInfo.InvariantCulture);
}
} catch (Exception ex) {
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1173"),
LongName), ex);
}
}
} else {
// this fails on mono if the _argumentValue is null
if (_argumentValue != null) {
_propertyInfo.SetValue(destination, _argumentValue, BindingFlags.Default, null, null, CultureInfo.InvariantCulture);
}
}
}
///
/// Assigns the specified value to the argument.
///
/// The value that should be assigned to the argument.
///
/// Duplicate argument.
/// -or-
/// Invalid value.
///
public void SetValue(string value) {
if (SeenValue && !AllowMultiple) {
throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1175"), LongName));
}
_seenValue = true;
object newValue = ParseValue(ValueType, value);
if (IsCollection || IsArray) {
if (Unique && _collectionValues.Contains(newValue)) {
throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1172"), value, LongName));
} else {
_collectionValues.Add(newValue);
}
} else if (IsNameValueCollection) {
// name/value pair is added to collection in ParseValue
} else {
_argumentValue = newValue;
}
}
#endregion Public Instance Methods
#region Private Instance Methods
private object ParseValue(Type type, string stringData) {
// null is only valid for bool variables
// empty string is never valid
if ((stringData != null || type == typeof(bool)) && (stringData == null || stringData.Length > 0)) {
try {
if (type == typeof(string)) {
return stringData;
} else if (type == typeof(bool)) {
if (stringData == null || stringData == "+") {
return true;
} else if (stringData == "-") {
return false;
}
} else if (IsNameValueCollectionType(type)) {
Match match = Regex.Match(stringData, @"(\w+[^=]*)=(\w*.*)");
if (match.Success) {
string name = match.Groups[1].Value;
string value = match.Groups[2].Value;
if (Unique && _valuePairs.Get(name) != null) {
// we always assume we're dealing with properties
// here to make the message more clear
throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1174"),
name, LongName));
}
_valuePairs.Add(name, value);
return _valuePairs;
} else {
throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1170"),
stringData, LongName), new ArgumentException(
"Expected name/value pair (=)."));
}
} else {
if (type.IsEnum) {
try {
return Enum.Parse(type, stringData, true);
} catch(ArgumentException ex) {
string message = "Invalid value {0} for command-line argument '-{1}'. Valid values are: ";
foreach (object value in Enum.GetValues(type)) {
message += value.ToString() + ", ";
}
// strip last ,
message = message.Substring(0, message.Length - 2) + ".";
throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture,
message, stringData, LongName), ex);
}
} else {
// Make a guess that the there's a public static Parse method on the type of the property
// that will take an argument of type string to convert the string to the type
// required by the property.
System.Reflection.MethodInfo parseMethod = type.GetMethod(
"Parse", BindingFlags.Public | BindingFlags.Static,
null, CallingConventions.Standard, new Type[] {typeof(string)},
null);
if (parseMethod != null) {
// Call the Parse method
return parseMethod.Invoke(null, BindingFlags.Default,
null, new object[] {stringData}, CultureInfo.InvariantCulture);
} else if (type.IsClass) {
// Search for a constructor that takes a string argument
ConstructorInfo stringArgumentConstructor =
type.GetConstructor(new Type[] {typeof(string)});
if (stringArgumentConstructor != null) {
return stringArgumentConstructor.Invoke(
BindingFlags.Default, null, new object[] {stringData},
CultureInfo.InvariantCulture);
}
}
}
}
} catch (CommandLineArgumentException) {
throw;
} catch (Exception ex) {
throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1170"),
stringData, LongName), ex);
}
}
throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA1170"), stringData,
LongName));
}
#endregion Private Instance Methods
#region Private Static Methods
private static CommandLineArgumentTypes GetArgumentType(CommandLineArgumentAttribute attribute, PropertyInfo propertyInfo) {
if (attribute != null) {
return attribute.Type;
} else if (IsCollectionType(propertyInfo.PropertyType)) {
return CommandLineArgumentTypes.MultipleUnique;
} else {
return CommandLineArgumentTypes.AtMostOnce;
}
}
private static Type GetElementType(PropertyInfo propertyInfo) {
Type elementType = null;
if (propertyInfo.PropertyType.IsArray) {
elementType = propertyInfo.PropertyType.GetElementType();
if (elementType == typeof(object)) {
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Property {0} is not a strong-typed array.", propertyInfo.Name));
}
} else if (typeof(ICollection).IsAssignableFrom(propertyInfo.PropertyType)) {
// Locate Add method with 1 parameter
foreach (MethodInfo method in propertyInfo.PropertyType.GetMethods(BindingFlags.Public | BindingFlags.Instance)) {
if (method.Name == "Add" && method.GetParameters().Length == 1) {
ParameterInfo parameter = method.GetParameters()[0];
if (parameter.ParameterType == typeof(object)) {
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Property {0} is not a strong-typed collection.", propertyInfo.Name));
} else {
elementType = parameter.ParameterType;
break;
}
}
}
if (elementType == null) {
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Invalid commandline argument type for property {0}.", propertyInfo.Name));
}
}
return elementType;
}
///
/// Indicates whether the specified is a
/// .
///
///
/// if can be assigned
/// to ; otherwise, .
///
private static bool IsNameValueCollectionType(Type type) {
return typeof(NameValueCollection).IsAssignableFrom(type);
}
///
/// Indicates whether the specified is collection-based.
///
///
/// if can be assigned
/// to and is not backed by a
/// that can be assigned to ;
/// otherwise, .
///
private static bool IsCollectionType(Type type) {
return typeof(ICollection).IsAssignableFrom(type)
&& !IsNameValueCollectionType(type);
}
///
/// Indicates whether the specified is an array.
///
///
/// if is an array;
/// otherwise, .
///
private static bool IsArrayType(Type type) {
return type.IsArray;
}
#endregion Private Static Methods
#region Private Instance Fields
private Type _elementType;
private bool _seenValue;
private CommandLineArgumentTypes _argumentType;
private PropertyInfo _propertyInfo;
private CommandLineArgumentAttribute _attribute;
private object _argumentValue;
private ArrayList _collectionValues;
private NameValueCollection _valuePairs;
#endregion Private Instance Fields
}
}