Skip to content

Commit

Permalink
Processing instructions transformations added #35
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeyzwezdin committed Dec 16, 2017
1 parent 63fccc2 commit 35b32f9
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 19 deletions.
37 changes: 33 additions & 4 deletions src/MagicChunks.Tests/Documents/XmlDocumentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -719,17 +719,21 @@ public void TransformProcessingInstructions()
var document = new XmlDocument(@"<Wix xmlns=""http://schemas.microsoft.com/wix/2006/wi"">
<?define ProductName=""My product for ArcGIS 10.2""?>
<?define ProductVersion=""1.3.0.0"" ?>
<?test value=""1"" ?>
</Wix>");

document.ReplaceKey(new[] { "Wix", "?define[@ProductName = 'My product for ArcGIS 10.2']", "@ProductName" }, "My product for ArcGIS 10.3");
document.ReplaceKey(new[] { "Wix", "?define[@ProductName = 'My product for ArcGIS 10.3']", "@ProductURL" }, "http://");
document.ReplaceKey(new[] { "Wix", "?test", "@value" }, "2");

var result = document.ToString();


// Assert
Assert.Equal(@"<Wix xmlns=""http://schemas.microsoft.com/wix/2006/wi"">
<?define ProductName=""My product for ArcGIS 10.3""?>
<?define ProductName=""My product for ArcGIS 10.3"" ProductURL=""http://""?>
<?define ProductVersion=""1.3.0.0"" ?>
<?test value=""2"" ?>
</Wix>", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);
}

Expand All @@ -738,27 +742,52 @@ public void TransformProcessingInstructions2()
{
// Arrange
var document = new XmlDocument(@"<Wix xmlns=""http://schemas.microsoft.com/wix/2006/wi"">
<?define ProductName=""My product for ArcGIS 10.2""?>
<?define ProductName=""My product for ArcGIS 10.2"" x=""2""?>
<?define ProductVersion=""1.3.0.0"" ?>
</Wix>");

document.ReplaceKey(new[] { "Wix", "Nested", "Nested2", "?define", "@ProductName" }, "My product for ArcGIS 10.3");
document.RemoveKey(new[] { "Wix", "?define[@ProductName = 'My product for ArcGIS 10.2']", "@x" });
document.RemoveKey(new[] { "Wix", "?define[@ProductVersion = '1.3.0.0']" });

var result = document.ToString();


// Assert
Assert.Equal(@"<Wix xmlns=""http://schemas.microsoft.com/wix/2006/wi"">
<?define ProductName=""My product for ArcGIS 10.2""?>
<?define ProductVersion=""1.3.0.0"" ?>
<Nested>
<Nested2>
<?define ProductName=""My product for ArcGIS 10.3""?>
</Nested2>
</Nested>
<?define ProductName=""My product for ArcGIS 10.2""?>
</Wix>", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);
}

[Fact]
public void TransformProcessingInstructions3()
{
// Arrange
var document = new XmlDocument(@"<Wix xmlns=""http://schemas.microsoft.com/wix/2006/wi"">
<?define ProductName=""My product for ArcGIS 10.2"" test=""1""?>
<?define ProductVersion=""1.3.0.0"" ?>
<?define ProductUrl=""1.3.0.0"" ?>
<?define ProductLicence=""1.3.0.0"" ?>
</Wix>");

document.ReplaceKey(new[] { "Wix", "?define[1]", "@test" }, "2");
document.RemoveKey(new[] { "Wix", "?define[0]", "@test" });
document.RemoveKey(new[] { "Wix", "?define[3]"});

var result = document.ToString();


// Assert
Assert.Equal(@"<Wix xmlns=""http://schemas.microsoft.com/wix/2006/wi"">
<?define ProductName=""My product for ArcGIS 10.2"" ?>
<?define ProductVersion=""1.3.0.0"" test=""2""?>
<?define ProductUrl=""1.3.0.0"" ?>
</Wix>", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);
}
}
}
119 changes: 104 additions & 15 deletions src/MagicChunks/Documents/XmlDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class XmlDocument : IDocument
{
private static readonly Regex AttributeFilterRegex = new Regex(@"(?<element>.+?)\[\s*\@(?<key>[\w\:]+)\s*\=\s*[\'\""]?(?<value>.+?)[\'\""]?\s*\]$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline);
private static readonly Regex ProcessingInstructionsPathElementRegex = new Regex(@"^\?.+", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline);
private static readonly Regex AttributePathElementRegex = new Regex(@"^\@.+", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline);
private static readonly Regex AttributeNodeRegex = new Regex(@"(?<attrName>\w+)\s*\=\s*\""(?<attrValue>.+?)\""", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline);

protected readonly XDocument Document;

Expand Down Expand Up @@ -47,7 +49,7 @@ public void AddElementToArray(string[] path, string value)

if (!path.Any(p => ProcessingInstructionsPathElementRegex.IsMatch(p)))
{
current = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace);
current = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XElement;
UpdateTargetArrayElement(value, path.Last(), current, documentNamespace);
}
else
Expand Down Expand Up @@ -75,12 +77,28 @@ public void ReplaceKey(string[] path, string value)

if (!path.Any(p => ProcessingInstructionsPathElementRegex.IsMatch(p)))
{
current = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace);
current = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XElement;
UpdateTargetElement(value, path.Last(), current, documentNamespace);
}
else
{
throw new NotImplementedException();
if (path.Take(path.Length - 2).Any(p => ProcessingInstructionsPathElementRegex.IsMatch(p)))
{
throw new ArgumentException("Processing instruction could not contain nested elements.", nameof(path));
}

if (!ProcessingInstructionsPathElementRegex.IsMatch(path.Skip(path.Length - 2).First()))
{
throw new ArgumentException("To update processing instruction you should point attribute name.", nameof(path));
}

if (!AttributePathElementRegex.IsMatch(path.Last()))
{
throw new ArgumentException("To update processing instruction you should point attribute name.", nameof(path));
}

var processingInstruction = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XProcessingInstruction;
UpdateProcessingInstruction(value, path.Last().TrimStart('@'), processingInstruction, documentNamespace);
}
}

Expand All @@ -103,41 +121,61 @@ public void RemoveKey(string[] path)

if (!path.Any(p => ProcessingInstructionsPathElementRegex.IsMatch(p)))
{
current = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace);
current = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XElement;
RemoveTargetElement(path.Last(), current, documentNamespace);
}
else
{
throw new NotImplementedException();
var processingInstruction = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XProcessingInstruction;

// Remove whole processing instruction (not just single attribute)
if (processingInstruction == null)
processingInstruction = FindPath(path.Skip(1).Take(path.Length - 1), current, documentNamespace) as XProcessingInstruction;

RemoveProcessingInstruction(path.Last(), processingInstruction, documentNamespace);
}
}

private static XElement FindPath(IEnumerable<string> path, XElement current, string documentNamespace)
private static XNode FindPath(IEnumerable<string> path, XElement current, string documentNamespace)
{
XNode result = current;

foreach (string pathElement in path)
{
if (pathElement.StartsWith("@"))
throw new ArgumentException("Attribute element could be only at end of the path.", nameof(path));

var currentElement = current?.GetChildElementByName(pathElement);
var currentElement = !ProcessingInstructionsPathElementRegex.IsMatch(pathElement) ? (result as XElement)?.GetChildElementByName(pathElement) as XNode : (result as XElement)?.GetChildProcessingInstructionByName(pathElement.TrimStart('?'));

var attributeFilterMatch = AttributeFilterRegex.Match(pathElement);
if (attributeFilterMatch.Success)
{
current = current.FindChildByAttrFilterMatch(attributeFilterMatch, documentNamespace);
if (!ProcessingInstructionsPathElementRegex.IsMatch(pathElement))
{
result = (result as XElement).FindChildByAttrFilterMatch(attributeFilterMatch, documentNamespace);
}
else
{
result = (result as XElement).FindProcessingInstructionByAttrFilterMatch(attributeFilterMatch, documentNamespace);
}
}
else if (currentElement != null)
{
current = currentElement;
result = currentElement;
}
else
{
if (!current.HasElements)
current.SetValue("");
current = current.CreateChildElement(documentNamespace, pathElement);
if (!(result as XElement).HasElements)
(result as XElement).SetValue("");

if (!ProcessingInstructionsPathElementRegex.IsMatch(pathElement))
result = (result as XElement).CreateChildElement(documentNamespace, pathElement);
else
result = (result as XElement).CreateChildProcessingInstruction(documentNamespace, pathElement.TrimStart('?'));
}
}
return current;

return result;
}

private static void UpdateTargetElement(string value, string targetElement, XElement current, string documentNamespace)
Expand All @@ -158,10 +196,10 @@ private static void UpdateTargetElement(string value, string targetElement, XEle
}
else
{
if(!current.HasElements)
if (!current.HasElements)
current.SetValue("");

current.Add(new XElement(targetElement.GetNameWithNamespace(current, documentNamespace)) {Value = value});
current.Add(new XElement(targetElement.GetNameWithNamespace(current, documentNamespace)) { Value = value });
}
}
else
Expand All @@ -171,6 +209,29 @@ private static void UpdateTargetElement(string value, string targetElement, XEle
}
}

private static void UpdateProcessingInstruction(string value, string targetElement, XProcessingInstruction current, string documentNamespace)
{
var valuesToReplace = AttributeNodeRegex.Matches(current.Data).OfType<Match>().Where(a =>
{
var eAttrName = a.Groups["attrName"]?.Value;
var eAttrValue = a.Groups["attrValue"]?.Value;

return String.Compare(eAttrName, targetElement, StringComparison.OrdinalIgnoreCase) == 0;
}).Select(m => m.Value).ToArray();

if (valuesToReplace.Any())
{
foreach (var replaceValue in valuesToReplace)
{
current.Data = current.Data.Replace(replaceValue, $"{targetElement}=\"{value}\"");
}
}
else
{
current.Data += (!string.IsNullOrEmpty(current.Data) ? " " : string.Empty) + $"{targetElement}=\"{value}\"";
}
}

private static void UpdateTargetArrayElement(string value, string targetElement, XElement current, string documentNamespace)
{
var attributeFilterMatch = AttributeFilterRegex.Match(targetElement);
Expand Down Expand Up @@ -222,6 +283,34 @@ private static void RemoveTargetElement(string targetElement, XElement current,
}
}

private static void RemoveProcessingInstruction(string targetElement, XProcessingInstruction current, string documentNamespace)
{
if (ProcessingInstructionsPathElementRegex.IsMatch(targetElement))
{
// remove whole processing restriction
current.Remove();
}
else if (AttributePathElementRegex.IsMatch(targetElement))
{
// remove attribute from processing instruction
var valuesToRemove = AttributeNodeRegex.Matches(current.Data).OfType<Match>().Where(a =>
{
var eAttrName = a.Groups["attrName"]?.Value;

return String.Compare(eAttrName, targetElement.TrimStart('@'), StringComparison.OrdinalIgnoreCase) == 0;
}).Select(m => m.Value).ToArray();

if (valuesToRemove.Any())
{
foreach (var value in valuesToRemove)
{
current.Data = current.Data.Replace(value, string.Empty);
}
}
}
}


public override string ToString()
{
return Document?.ToStringWithDeclaration() ?? String.Empty;
Expand Down
74 changes: 74 additions & 0 deletions src/MagicChunks/Helpers/XmlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace MagicChunks.Helpers
public static class XmlExtensions
{
private static readonly Regex NodeIndexEndingRegex = new Regex(@"\[\d+\]$", RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly Regex ProcessingInstructionsPathElementRegex = new Regex(@"^\?.+", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline);
private static readonly Regex AttributeNodeRegex = new Regex(@"(?<attrName>\w+)\s*\=\s*\""(?<attrValue>.+?)\""", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline);

public static string ToStringWithDeclaration(this XDocument document)
{
Expand Down Expand Up @@ -100,14 +102,67 @@ public static XElement GetChildElementByName(this XElement source, string name)
}
}

public static XProcessingInstruction GetChildProcessingInstructionByName(this XElement source, string name)
{
if (!NodeIndexEndingRegex.IsMatch(name))
{
return source?.Nodes().OfType<XProcessingInstruction>()
.FirstOrDefault(e => String.Compare(e.Target, name, StringComparison.OrdinalIgnoreCase) == 0);
}
else
{
string nodeName;
int nodeIndex;

try
{
nodeName = NodeIndexEndingRegex.Replace(name, String.Empty);
nodeIndex = int.Parse(NodeIndexEndingRegex.Match(name).Value.Trim('[', ']'));

if (nodeIndex < 0)
throw new ArgumentException("Index should be greater than 0.");
}
catch (ArgumentException ex)
{
throw new ArgumentException($"Wrong element name: {name}", ex);
}
catch (FormatException ex)
{
throw new ArgumentException($"Wrong element name: {name}", ex);
}
catch (OverflowException ex)
{
throw new ArgumentException($"Wrong element name: {name}", ex);
}

var elements = source?.Nodes().OfType<XProcessingInstruction>()
.Where(e => String.Compare(e.Target, nodeName, StringComparison.OrdinalIgnoreCase) == 0);

return elements.Skip(nodeIndex).FirstOrDefault();
}
}

public static XElement GetChildElementByAttrValue(this XElement source, string name, string attr, string attrValue)
{
var elements = source.Elements()
.Where(e => String.Compare(e.Name.LocalName, name, StringComparison.OrdinalIgnoreCase) == 0);

return elements
.FirstOrDefault(e => e.Attributes().Any(a => (a.Name == attr.GetNameWithNamespace(source, String.Empty)) && (a.Value == attrValue)));
}

public static XProcessingInstruction GetChildProcessingInstructionByAttrValue(this XElement source, string name, string attr, string attrValue)
{
var nodes = source.Nodes().OfType<XProcessingInstruction>()
.Where(e => String.Compare(e.Target, name, StringComparison.OrdinalIgnoreCase) == 0);

return nodes.FirstOrDefault(e => AttributeNodeRegex.Matches(e.Data).OfType<Match>().Any(a =>
{
var eAttrName = a.Groups["attrName"]?.Value;
var eAttrValue = a.Groups["attrValue"]?.Value;

return (String.Compare(eAttrName, attr, StringComparison.OrdinalIgnoreCase) == 0) && (eAttrValue == attrValue);
}));
}

public static XElement CreateChildElement(this XElement source, string documentNamespace, string elementName,
Expand All @@ -124,6 +179,14 @@ public static XElement CreateChildElement(this XElement source, string documentN
return item;
}

public static XProcessingInstruction CreateChildProcessingInstruction(this XElement source, string documentNamespace, string elementName,
string attrName = null, string attrValue = null)
{
var item = new XProcessingInstruction(elementName, (!string.IsNullOrWhiteSpace(attrName) && !string.IsNullOrWhiteSpace(attrValue)) ? $"{attrName}=\"{attrValue}\"" : string.Empty);
source.Add(item);
return item;
}

public static XElement FindChildByAttrFilterMatch(this XElement source, Match attributeFilterMatch,
string documentNamespace)
{
Expand All @@ -134,5 +197,16 @@ public static XElement FindChildByAttrFilterMatch(this XElement source, Match at
var item = source?.GetChildElementByAttrValue(elementName, attrName, attrValue);
return item ?? source.CreateChildElement(documentNamespace, elementName, attrName, attrValue);
}

public static XProcessingInstruction FindProcessingInstructionByAttrFilterMatch(this XElement source, Match attributeFilterMatch,
string documentNamespace)
{
var elementName = attributeFilterMatch.Groups["element"].Value?.TrimStart('?');
var attrName = attributeFilterMatch.Groups["key"].Value;
var attrValue = attributeFilterMatch.Groups["value"].Value;

var item = source?.GetChildProcessingInstructionByAttrValue(elementName, attrName, attrValue);
return item ?? source.CreateChildProcessingInstruction(documentNamespace, elementName, attrName, attrValue);
}
}
}

0 comments on commit 35b32f9

Please sign in to comment.