using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; namespace OpenTap.Package { [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] internal class DependsOnAttribute : Attribute { public Type Dependent { get; set; } public DependsOnAttribute(Type dependent) { if (TypeData.FromType(dependent).DescendsTo(typeof(IElementExpander)) == false) throw new Exception($"Type '{dependent.Name}' is not an {nameof(IElementExpander)}."); Dependent = dependent; } } internal interface IElementExpander { /// /// Expand all attributes and text on the input element /// /// /// void Expand(XElement element); } [DependsOn(typeof(VariableExpander))] [DependsOn(typeof(EnvironmentVariableExpander))] [DependsOn(typeof(GitVersionExpander))] [DependsOn(typeof(DefaultVariableExpander))] internal class ConditionExpander : IElementExpander { private static TraceSource log = Log.CreateSource("Condition Evaluator"); /// /// Evaluate any 'Condition' attribute and remove the element if it is not satisfied /// Otherwise remove the condition /// /// public void Expand(XElement element) { foreach (var condition in element.Attributes("Condition").ToArray()) { if (EvaluateCondition(condition) && condition.Parent != null) condition.Remove(); else { if (element.Parent != null) element.Remove(); break; } } } internal string GetExpansion(string condition) { string normalize(string str) { // Trim leading and trailing space str = str.Trim(); if (str == string.Empty) return string.Empty; // Remove one level of quotes if present if ((str[0] == '\'' || str[0] == '"') && str.First() == str.Last()) str = str.Substring(1, str.Length - 2); return str; } // Check if the condition is a literal var norm = normalize(condition); if (string.IsNullOrWhiteSpace(norm)) return string.Empty; if (norm.IndexOf("==", StringComparison.Ordinal) < 0 && norm.IndexOf("!=", StringComparison.Ordinal) < 0) return norm; condition = condition.Trim(); var parts = condition.Split(new string[] { "==", "!=" }, StringSplitOptions.None) .Select(p => p.Trim()) .ToArray(); var lhs = normalize(parts[0]); var rhs = normalize(parts[1]); var isEquals = condition.IndexOf("==", StringComparison.Ordinal) >= 0; var areEqual = lhs.Equals(rhs, StringComparison.Ordinal); return isEquals == areEqual ? "true" : string.Empty; } bool EvaluateCondition(XAttribute attr) { var condition = attr.Value; var result = GetExpansion(condition).Any(); if (attr.Parent is IXmlLineInfo li && li.HasLineInfo()) { log.Debug($@"XML Line {li.LineNumber}: Evaluated Condition=""{condition}"" to ""{result}"""); } return result; } } [DependsOn(typeof(GitVersionExpander))] internal class VariableExpander : IElementExpander { private static TraceSource log = Log.CreateSource(nameof(VariableExpander)); public VariableExpander(ElementExpander s) { Stack = s; } internal void InitVariables(IEnumerable variablesGroup) { foreach (var propertyGroup in variablesGroup) { var desc = propertyGroup.Descendants().ToArray(); foreach (var variable in desc) { var k = variable.Name.LocalName; // Overriding an existing key is fine here // It could be intentional. E.g. // $(PATH):abc // followed by // $(PATH):def Stack.ExpandElement(variable); // The variable may have been removed from the document if its condition was 'false' // In this case, do not add its value as a property. if (variable.Parent != null) Variables[k] = variable.Value; } } log.Debug($"Initialized variable expander:"); foreach (var kvp in Variables) { log.Debug($"{kvp.Key} = '{kvp.Value}'"); } } private Dictionary Variables { get; } = new Dictionary(); public ElementExpander Stack { get; set; } public void Expand(XElement element) { foreach (var key in Variables.Keys) { ExpansionHelper.ReplaceToken(element, key, Variables[key].ToString()); } } } [DependsOn(typeof(GitVersionExpander))] [DependsOn(typeof(VariableExpander))] internal class EnvironmentVariableExpander : IElementExpander { public EnvironmentVariableExpander() { Variables = Environment.GetEnvironmentVariables(); Keys = Variables.Keys.OfType().ToArray(); } private IDictionary Variables { get; } private string[] Keys { get; } public void Expand(XElement element) { foreach (var key in Keys) { ExpansionHelper.ReplaceToken(element, key, Variables[key].ToString()); } } } [DependsOn(typeof(GitVersionExpander))] [DependsOn(typeof(VariableExpander))] [DependsOn(typeof(EnvironmentVariableExpander))] internal class DefaultVariableExpander : IElementExpander { private static Regex VariableRegex = new Regex("\\$!?\\(.*?\\)"); public HashSet UndefinedVariables = new HashSet(StringComparer.OrdinalIgnoreCase); /// /// Replace all variables with an empty string. E.g. '$(whatever) -> '')' /// /// public void Expand(XElement element) { var textNodes = element.Nodes().OfType().ToArray(); foreach (var textNode in textNodes) { foreach (Match match in VariableRegex.Matches(textNode.Value)) { if (match.Value.StartsWith("$!(")) UndefinedVariables.Add(match.Value.Substring(3, match.Value.Length - 4)); textNode.Value = textNode.Value.Replace(match.Value, ""); } } var attributeNodes = element.Attributes().ToArray(); foreach (var attribute in attributeNodes) { foreach (Match match in VariableRegex.Matches(attribute.Value)) { if (match.Value.StartsWith("$!(")) UndefinedVariables.Add(match.Value.Substring(3, match.Value.Length - 4)); attribute.Value = attribute.Value.Replace(match.Value, ""); } } } } internal class GitVersionExpander : IElementExpander { public GitVersionExpander(string projectDir) { ProjectDir = projectDir; } private string ProjectDir { get; } private string longVersion = null; private string version = null; private static TraceSource log = Log.CreateSource(nameof(GitVersionExpander)); public void Expand(XElement element) { ReplaceVersion("GitVersion", 5, ref version); ReplaceVersion("GitLongVersion", 4, ref longVersion); void ReplaceVersion(string versionName, int fieldCount, ref string cachedVersion) { if (!element.ToString().Contains("$(" + versionName + ")")) return; if (cachedVersion == null && string.IsNullOrWhiteSpace(ProjectDir) == false) { try { var calc = new GitVersionCalulator(ProjectDir); cachedVersion = calc.GetVersion().ToString(fieldCount); log.Info("Package {1} is {0}", cachedVersion, versionName); } catch (Exception ex) { log.ErrorOnce(cachedVersion, "Failed to calculate {0}.", versionName); log.Debug(ex); } } // If 'GitVersion' could not be resolved, don't replace it if (cachedVersion != null) ExpansionHelper.ReplaceToken(element, versionName, cachedVersion); } } } }