// NAnt - A .NET build tool // Copyright (C) 2003 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 // // Gerry Shaw (gerry_shaw@yahoo.com) // Tomas Restrepo (tomasr@mvps.org) // Gert Driesen (gert.driesen@ardatis.com) using System; using System.Collections; using System.Collections.Specialized; using System.Globalization; using System.Text; using System.Text.RegularExpressions; using NAnt.Core.Util; namespace NAnt.Core { [Serializable()] public class PropertyDictionary : DictionaryBase { #region Public Instance Constructors /// /// Initializes a new instance of the /// class holding properties for the given /// instance. /// /// The project for which the dictionary will hold properties. public PropertyDictionary(Project project){ _project = project; } #endregion Public Instance Constructors #region Public Instance Properties /// /// Indexer property. /// public virtual string this[string name] { get { string value = (string) Dictionary[(object) name]; // check whether (built-in) property is deprecated CheckDeprecation(name); if (IsDynamicProperty(name)) { return ExpandProperties(value, Location.UnknownLocation); } else { return value; } } set { Dictionary[name] = value; } } /// /// Gets the project for which the dictionary holds properties. /// /// /// The project for which the dictionary holds properties. /// public Project Project { get { return _project; } } #endregion Public Instance Properties #region Override implementation of DictionaryBase protected override void OnClear() { _readOnlyProperties.Clear(); _dynamicProperties.Clear(); } protected override void OnSet(object key, object oldValue, object newValue) { // at this point we're sure the key is valid, as it has already // been verified by OnValidate string propertyName = (string) key; // do not allow value of read-only property to be overwritten if (IsReadOnlyProperty(propertyName)) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1068"), propertyName), Location.UnknownLocation); } base.OnSet(key, oldValue, newValue); } /// /// Performs additional custom processes before inserting a new element /// into the instance. /// /// The key of the element to insert. /// The value of the element to insert. protected override void OnInsert(object key, object value) { // at this point we're sure the key is valid, as it has already // been verified by OnValidate string propertyName = (string) key; // ensure property doesn't already exist if (Contains(propertyName)) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1065"), propertyName), Location.UnknownLocation); } } /// /// Performs additional custom processes before removing an element /// from the instance. /// /// The key of the element to remove. /// The value of the element to remove. protected override void OnRemove(object key, object value) { string propertyName = key as string; if (propertyName != null && _readOnlyProperties.Contains (propertyName)) { _readOnlyProperties.Remove (propertyName); } } /// /// Performs additional custom processes when validating the element /// with the specified key and value. /// /// The key of the element to validate. /// The value of the element to validate. protected override void OnValidate(object key, object value) { string propertyName = key as string; if (propertyName == null) { throw new ArgumentException("Property name must be a string.", "key"); } ValidatePropertyName(propertyName, Location.UnknownLocation); ValidatePropertyValue(value, Location.UnknownLocation); base.OnValidate(key, value); } #endregion Override implementation of DictionaryBase #region Public Instance Methods /// /// Adds a property that cannot be changed. /// /// The name of the property. /// The value to assign to the property. /// /// Properties added with this method can never be changed. Note that /// they are removed if the method is called. /// public virtual void AddReadOnly(string name, string value) { if (!IsReadOnlyProperty(name)) { Dictionary.Add(name, value); _readOnlyProperties.Add(name); } } /// /// Marks a property as a property of which the value is expanded at /// execution time. /// /// The name of the property to mark as dynamic. public virtual void MarkDynamic(string name) { if (!IsDynamicProperty(name)) { // check if the property actually exists if (!Contains(name)) { throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1067"))) ; } _dynamicProperties.Add(name); } } /// /// Adds a property to the collection. /// /// The name of the property. /// The value to assign to the property. public virtual void Add(string name, string value) { Dictionary.Add(name, value); } /// /// Determines whether the specified property is listed as read-only. /// /// The name of the property to check. /// /// if the property is listed as read-only; /// otherwise, . /// public virtual bool IsReadOnlyProperty(string name) { return _readOnlyProperties.Contains(name); } /// /// Determines whether the specified property is listed as dynamic. /// /// The name of the property to check. /// /// if the property is listed as dynamic; /// otherwise, . /// public virtual bool IsDynamicProperty(string name) { return _dynamicProperties.Contains(name); } /// /// Inherits properties from an existing property dictionary Instance. /// /// Property list to inherit. /// The list of properties to exclude during inheritance. public virtual void Inherit(PropertyDictionary source, StringCollection excludes) { foreach (DictionaryEntry entry in source.Dictionary) { string propertyName = (string) entry.Key; if (excludes != null && excludes.Contains(propertyName)) { continue; } // do not overwrite an existing read-only property if (IsReadOnlyProperty(propertyName)) { continue; } // add property to dictionary ValidatePropertyName(propertyName, Location.UnknownLocation); Dictionary[propertyName] = entry.Value; // if property is readonly, add to collection of readonly properties if (source.IsReadOnlyProperty(propertyName)) { _readOnlyProperties.Add(propertyName); } // if property is dynamic, add to collection of dynamic properties // if it was not already in that collection if (source.IsDynamicProperty(propertyName) && !IsDynamicProperty(propertyName)) { _dynamicProperties.Add(propertyName); } } } /// /// Expands a from known properties. /// /// The replacement tokens. /// The to pass through for any exceptions. /// The expanded and replaced string. public string ExpandProperties(string input, Location location) { Hashtable state = new Hashtable(); Stack visiting = new Stack(); return ExpandProperties(input, location, state, visiting); } /// /// Determines whether a property already exists. /// /// The name of the property to check. /// /// if the specified property already exists; /// otherwise, . /// public bool Contains(string name) { return Dictionary.Contains(name); } /// /// Removes the property with the specified name. /// /// The name of the property to remove. public void Remove(string name) { Dictionary.Remove(name); } #endregion Public Instance Methods #region Internal Instance Methods internal string GetPropertyValue(string propertyName) { // check whether (built-in) property is deprecated CheckDeprecation(propertyName); return (string) Dictionary[propertyName]; } /// /// Expands a from known properties. /// /// The replacement tokens. /// The to pass through for any exceptions. /// A mapping from properties to states. The states in question are "VISITING" and "VISITED". Must not be . /// A stack of properties which are currently being visited. Must not be . /// The expanded and replaced string. internal string ExpandProperties(string input, Location location, Hashtable state, Stack visiting) { return EvaluateEmbeddedExpressions(input, location, state, visiting); } #endregion Internal Instance Methods #region Private Instance Methods /// /// Evaluates the given expression string and returns the result /// /// /// /// /// /// private string EvaluateEmbeddedExpressions(string input, Location location, Hashtable state, Stack visiting) { if (input == null) { return null; } if (input.IndexOf('$') < 0) { return input; } try { StringBuilder output = new StringBuilder(input.Length); ExpressionTokenizer tokenizer = new ExpressionTokenizer(); ExpressionEvaluator eval = new ExpressionEvaluator(Project, this, state, visiting); tokenizer.IgnoreWhitespace = false; tokenizer.SingleCharacterMode = true; tokenizer.InitTokenizer(input); while (tokenizer.CurrentToken != ExpressionTokenizer.TokenType.EOF) { if (tokenizer.CurrentToken == ExpressionTokenizer.TokenType.Dollar) { tokenizer.GetNextToken(); if (tokenizer.CurrentToken == ExpressionTokenizer.TokenType.LeftCurlyBrace) { tokenizer.IgnoreWhitespace = true; tokenizer.SingleCharacterMode = false; tokenizer.GetNextToken(); string val = Convert.ToString(eval.Evaluate(tokenizer), CultureInfo.InvariantCulture); output.Append(val); tokenizer.IgnoreWhitespace = false; if (tokenizer.CurrentToken != ExpressionTokenizer.TokenType.RightCurlyBrace) { throw new ExpressionParseException("'}' expected", tokenizer.CurrentPosition.CharIndex); } tokenizer.SingleCharacterMode = true; tokenizer.GetNextToken(); } else { output.Append('$'); if (tokenizer.CurrentToken != ExpressionTokenizer.TokenType.EOF) { output.Append(tokenizer.TokenText); tokenizer.GetNextToken(); } } } else { output.Append(tokenizer.TokenText); tokenizer.GetNextToken(); } } return output.ToString(); } catch (ExpressionParseException ex) { StringBuilder errorMessage = new StringBuilder(); string reformattedInput = input; // replace CR, LF and TAB with a space reformattedInput = reformattedInput.Replace('\n', ' '); reformattedInput = reformattedInput.Replace('\r', ' '); reformattedInput = reformattedInput.Replace('\t', ' '); errorMessage.Append(ex.Message); errorMessage.Append(Environment.NewLine); string label = "Expression: "; errorMessage.Append(label); errorMessage.Append(reformattedInput); int p0 = ex.StartPos; int p1 = ex.EndPos; if (p0 != -1 || p1 != -1) { errorMessage.Append(Environment.NewLine); if (p1 == -1) p1 = p0 + 1; for (int i = 0; i < p0 + label.Length; ++i) errorMessage.Append(' '); for (int i = p0; i < p1; ++i) errorMessage.Append('^'); } throw new BuildException(errorMessage.ToString(), location, ex.InnerException); } } /// /// Checks whether the specified property is deprecated. /// /// The property to check. private void CheckDeprecation(string name) { switch (name) { case Project.NAntPropertyFileName: Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use assembly::get-location(nant::get-assembly()) expression instead.", name); break; case Project.NAntPropertyVersion: Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the assemblyname::get-version(assembly::get-name(nant::get-assembly))" + " expression instead.", name); break; case Project.NAntPropertyLocation: Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the nant::get-base-directory() function instead.", name); break; case Project.NAntPropertyProjectBaseDir: Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the project::get-base-directory() function instead.", name); break; case Project.NAntPropertyProjectName: Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the project::get-name() function instead.", name); break; case Project.NAntPropertyProjectBuildFile: Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the project::get-buildfile-uri() function" + " instead.", name); break; case Project.NAntPropertyProjectDefault: Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the project::get-default-target() function" + " instead.", name); break; case Project.NAntPlatformName: Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the platform::get-name() function instead.", name); break; case Project.NAntPlatform + ".win32": Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the platform::is-win32() function instead.", name); break; case Project.NAntPlatform + ".unix": Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the platform::is-unix() function instead.", name); break; case "nant.settings.currentframework.description": Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the framework::get-description(framework::get-target-framework())" + " function instead.", name); break; case "nant.settings.currentframework.frameworkdirectory": Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the framework::get-framework-directory(framework::get-target-framework())" + " function instead.", name); break; case "nant.settings.currentframework.sdkdirectory": Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the framework::get-sdk-directory(framework::get-target-framework())" + " function instead.", name); break; case "nant.settings.currentframework.frameworkassemblydirectory": Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the framework::get-assembly-directory(framework::get-target-framework())" + " function instead.", name); break; case "nant.settings.currentframework.runtimeengine": Project.Log(Level.Warning, "Built-in property '{0}' is deprecated." + " Use the framework::get-runtime-engine(framework::get-target-framework())" + " function instead.", name); break; default: if (name.StartsWith("nant.tasks.")) { Project.Log(Level.Warning, "Built-in property '{0}' is" + " deprecated. Use the task::exists(string) function" + " instead.", name); } break; } } #endregion Private Instance Methods #region Private Static Methods private static void ValidatePropertyName(string propertyName, Location location) { const string propertyNamePattern = "^[_A-Za-z0-9][_A-Za-z0-9\\-.]*$"; // validate property name // if (!Regex.IsMatch(propertyName, propertyNamePattern)) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1064"), propertyName), location); } if (propertyName.EndsWith("-") || propertyName.EndsWith(".")) { // this additional rule helps simplify the regex pattern throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1064"), propertyName), location); } } private static void ValidatePropertyValue(object value, Location location) { if (value != null) { if (!(value is string)) { throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1066"), value.GetType()), "value"); } } else { // TODO: verify this // throw new ArgumentException("Property value '" + propertyName + "' must not be null", "value"); return; } } #endregion Private Static Methods #region Internal Static Methods /// /// Builds an appropriate exception detailing a specified circular /// reference. /// /// The property reference to stop at. Must not be . /// A stack of property references. Must not be . /// /// A detailing the specified circular /// dependency. /// internal static BuildException CreateCircularException(string end, Stack stack) { StringBuilder sb = new StringBuilder("Circular property reference: "); sb.Append(end); string c; do { c = (string) stack.Pop(); sb.Append(" <- "); sb.Append(c); } while (!c.Equals(end)); return new BuildException(sb.ToString()); } #endregion Internal Static Methods #region Private Instance Fields /// /// Maintains a list of the property names that are readonly. /// private StringCollection _readOnlyProperties = new StringCollection(); /// /// Maintains a list of the property names of which the value is expanded /// on usage, not at initalization. /// private StringCollection _dynamicProperties = new StringCollection(); /// /// The project for which the dictionary holds properties. /// private readonly Project _project; #endregion Private Instance Fields #region Internal Static Fields /// /// Constant for the "visiting" state, used when traversing a DFS of /// property references. /// internal const string Visiting = "VISITING"; /// /// Constant for the "visited" state, used when travesing a DFS of /// property references. /// internal const string Visited = "VISITED"; #endregion Internal Static Fields } }