using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace OpenTap.Package
{
///
/// The class can expand variables of the form $(VarName) -> VarNameValue
/// in an XML document. It reads the current environment variables, and optionally document-local variables set in
/// a element as a child of the root element. A variable will be expanded exactly once either if it is
/// an XMLText element, or if it appears as text in an attribute.
/// (e.g. abc $(def) ghi will expand $(abc) and $(def))
/// A variable will only be expanded once. If $(abc) -> "$(def)", then the expansion of $(abc) will not be expanded.
/// Additionally, 'Conditions' are supported. Conditions are attributes on elements. If the condition evaluates to
/// false, the element containing the condition is removed from the document. If the condition is true, the
/// condition itself is removed. A condition takes the form Condition="$(abc)" or Condition="$(abc) == $(def)" or
/// Condition="$(abc) != $(def)". A condition is true if it has the value '1' or 'true', or if the comparison operator evaluates to true.
///
internal class PackageXmlPreprocessor
{
///
/// Initializes a new instance of from an object.
///
///
///
public PackageXmlPreprocessor(XElement root, string projectPath = null)
{
// Create a deep copy of the source element
Root = new XElement(root);
InitExpander(projectPath);
}
///
/// Initializes a new instance of from a file path.
///
///
///
public PackageXmlPreprocessor(string path, string projectPath = null)
{
if (File.Exists(path) == false) throw new FileNotFoundException($"The file '{path}' does not exist.");
try
{
Root = XElement.Load(path, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
}
catch (Exception)
{
log.Debug($"Error loading XML Document from file '{path}'.");
throw;
}
InitExpander(projectPath);
}
private readonly DefaultVariableExpander _defaultExpander = new DefaultVariableExpander();
private void InitExpander(string projectPath = null)
{
// The project directory is required to compute the gitversion. If it is not set, GitVersion will not be computed.
Expander.AddProvider(new GitVersionExpander(projectPath));
Expander.AddProvider(_defaultExpander);
Expander.AddProvider(new EnvironmentVariableExpander());
Expander.AddProvider(new ConditionExpander());
// Evaluate all ' elements and remove them from the document
var variables = Root.Elements(Root.GetDefaultNamespace().GetName("Variables")).ToArray();
var variableExpander = new VariableExpander(Expander);
Expander.AddProvider(variableExpander);
variableExpander.InitVariables(variables);
foreach (var propElem in variables)
{
// The property could have been removed due to a Condition.
if (propElem.Parent != null)
propElem.Remove();
}
}
private ElementExpander Expander { get; } = new ElementExpander();
///
/// Evaluate all variables of the form $(VarName) -> VarNameValue in the document.
/// Evaluate all Conditions of the form 'Condition="Some-Condition-Expression"'
/// Removes the node which contains the condition if it evaluates to false. Otherwise it removes the condition itself.
///
public XElement Evaluate()
{
ExpandNodeRecursive(Root);
// OpenTAP only supports a single , and element,
// but with conditions it makes sense to specify these elements multiple times with different conditions.
// Their children should be merged in a single parent element so the XML is still valid according to the schema.
MergeDuplicateElements(Root);
if (_defaultExpander.UndefinedVariables.Any())
throw new Exception(
$"The following required variables are not defined: {string.Join(", ", _defaultExpander.UndefinedVariables)}");
return Root;
}
static TraceSource log = Log.CreateSource(nameof(PackageXmlPreprocessor));
XElement Root { get; }
///
/// Merge all top-level elements in elem by adding all of the children of duplicate elements to
/// the first element
///
///
static void MergeDuplicateElements(XElement elem)
{
var remaining = elem.Elements().GroupBy(e => e.Name.LocalName);
foreach (var rem in remaining)
{
var main = rem.First();
var rest = rem.Skip(1).ToArray();
foreach (var dup in rest)
{
foreach (var child in dup.Elements().ToArray())
{
main.Add(new XElement(child));
}
dup.Remove();
}
}
}
///
/// Recursively expand the children of ele until a terminal node is reached.
///
///
void ExpandNodeRecursive(XElement ele)
{
Expander.ExpandElement(ele);
foreach (var desc in ele.Elements().ToArray())
{
ExpandNodeRecursive(desc);
}
}
}
}