public void ctor_NullSpecs_NullNodeDefs() { //arrange var actual = new XmlNodePath(null as string); //act //assert actual.NodeDefs.Should().BeNull(); }
public void ctor_EmptySpecs_NoNodeDefs() { //arrange var actual = new XmlNodePath(string.Empty); //act //assert actual.NodeDefs.Count.Should().Be(0); }
public void TryRemoveAt_is_successful(string expectedAfter, string nodePath, string removedFragment, string before) { var beforeDoc = ParseDoc(before); var expectedAfterDoc = ParseDoc(expectedAfter); var path = XmlNodePath.Parse(nodePath); var removedCompareNode = ParseDoc(string.Format("<r>{0}</r>", removedFragment)).Root.Nodes().Single(); removedCompareNode.Remove(); Assert.That(beforeDoc.TryRemove(path, removedCompareNode), Is.True); Assert.That(beforeDoc.DeepEquals(expectedAfterDoc), Is.True); }
public void TryInsertAt_is_successful(string before, string nodePath, string insertedFragment, string expectedAfter) { var beforeDoc = ParseDoc(before); var expectedAfterDoc = ParseDoc(expectedAfter); var path = XmlNodePath.Parse(nodePath); var inserted = ParseDoc(string.Format("<r>{0}</r>", insertedFragment)).Root.Nodes().Single(); inserted.Remove(); Assert.That(beforeDoc.TryInsert(path, inserted), Is.True); Assert.That(beforeDoc.DeepEquals(expectedAfterDoc), Is.True); }
/// <summary> /// Creates an instance of an XML dispenser for a single target. /// </summary> /// <param name="writer"></param> /// <param name="targetNo">1 based target number.</param> /// <param name="settings"></param> /// <param name="outputIsAsync"></param> internal XmlDispenserForTarget(TextWriter writer, int targetNo, string settings, bool outputIsAsync) : base(writer, targetNo) //Note that writer is passed to base class, even though it is not used there (all relevant methods are overridden, so null could've been passed instead). //This is because base.Dispose (called by Dispose in this class) disposes the writer (it is unclear whether XmlWriter.Dispose disposes underlying writer). { var settingDict = settings?.SplitPairsToListOfTuples()?.ToDictionary(t => t.Item1, t => t.Item2); // Discrete settings to unpack: // CollectionNode - "xpath" defining the collection of clusters/records (may be null/empty, in which case output will contain XML fragment where each root constitutes record or cluster). // ClusterNode - "xpath" defining clusters node within collection node (null/empty means record nodes are directly inside collection node). // RecordNode - "xpath" defining record node within cluster node (or collection node if cluster node is empty). RecordNode is mandatory, if absent "__record__" is assumed. // AttributeFields - a semicolon-separated list of field names (item keys) to be projected into XML as attributes of the record node (and not inner nodes). // IndentChars - string to use when indenting, e.g. "\t" or " "; allows "pretty-print" XML output; when absent, no indenting takes place. // NewLineChars - allows "pretty-print" XML output // "xpath" is always relative (no need for ./), each of the nodes is separated by /. _collNodePath = new XmlNodePath(settingDict.GetStringSetting("CollectionNode")); _clstrNodePath = new XmlNodePath(settingDict.GetStringSetting("ClusterNode")); _recNodePath = new XmlNodePath(settingDict.GetStringSetting("RecordNode")); if (_recNodePath.IsEmpty) { _recNodePath = new XmlNodePath("__record__"); //RecordNode is mandatory; if absent in config, __record__ is assumed } var attrFlds = settingDict.GetStringSetting("AttributeFields")?.ToListOfStrings(';'); _attributeFields = attrFlds == null ? new HashSet <string>() : new HashSet <string>(attrFlds); var indentChars = settingDict.GetStringSetting("IndentChars"); var newLineChars = settingDict.GetStringSetting("NewLineChars"); _observeClusters = !_clstrNodePath.IsEmpty; _clusterDepth = _observeClusters ? _clstrNodePath.NodeDefs.Count : 0; var writerSettings = new XmlWriterSettings() { ConformanceLevel = _collNodePath.IsEmpty ? ConformanceLevel.Fragment : ConformanceLevel.Document }; if (indentChars != null) { writerSettings.Indent = true; writerSettings.IndentChars = indentChars; } if (newLineChars != null) { writerSettings.NewLineChars = newLineChars; } writerSettings.Async = outputIsAsync; _xmlWriter = XmlWriter.Create(writer, writerSettings); _currClstrNo = 0; //will stay at 0 (undetermined) unless ClusterNode defined _atStart = true; }
/// <summary> /// Asynchronously write a series of starting nodes based on the path given /// </summary> /// <param name="nodesToWrite"></param> /// <returns></returns> private async Task WriteStartNodesAsync(XmlNodePath nodesToWrite) { //note that XmlWriter remembers all nodes that were started, so there is no need to remember them Debug.Assert(!nodesToWrite.IsEmpty); _ = nodesToWrite.NodeDefs.Skip(1).Any() ? new XmlNodePath(nodesToWrite.NodeDefs.Skip(1)) //the remaining nodes to write during recursive calls : null; //end of recursion await WriteStartNodeAsync(nodesToWrite.NodeDefs[0]); //head if (nodesToWrite.NodeDefs.Skip(1).Any()) //tail { //tail exists; inner node(s) are written using recursion await WriteStartNodesAsync(new XmlNodePath(nodesToWrite.NodeDefs.Skip(1))); } }
public void ctor_SimpleSpecs_CorrectData() { //arrange var actual = new XmlNodePath("TheOnlyNode"); //act var nodeDefs = actual.NodeDefs; //assert nodeDefs.Count.Should().Be(1); var nodeDef = nodeDefs[0]; nodeDef.Name.Should().Be("TheOnlyNode"); nodeDef.GetAttributes().Count.Should().Be(0); }
public void IsEmpty_EmptyNode_ReturnsTrue() { //arrange var actual = new XmlNodePath(null as string); var actual2 = new XmlNodePath(null as List <XmlNodeDef>); var actual3 = new XmlNodePath(string.Empty); //act //assert actual.NodeDefs.Should().BeNull(); actual.IsEmpty.Should().BeTrue(); actual2.NodeDefs.Should().BeNull(); actual2.IsEmpty.Should().BeTrue(); actual3.NodeDefs.Count.Should().Be(0); //empty string will result in empty list actual3.IsEmpty.Should().BeTrue(); }
/// <summary> /// Asynchronously send items of the current record to XML output. /// </summary> /// <param name="xrecNodePath"></param> /// <param name="line"></param> /// <returns></returns> private async Task WriteXrecordAsync(XmlNodePath xrecNodePath, ExternalLine line) { Debug.Assert(line.GetType() == typeof(Xrecord)); var head = xrecNodePath.NodeDefs[0]; var tail = xrecNodePath.NodeDefs.Skip(1).Any() ? new XmlNodePath(xrecNodePath.NodeDefs.Skip(1)) //XmlNodePath representing the remaining node patterns : null; //end of recursion await _xmlWriter.WriteStartElementAsync(null, head.Name, null); //start element var attrsToWrite = head.GetAttributes(); if (tail == null) //leaf level (end of recursion) { //Don't write records right away as some items may need to be written as attributes (and not inner nodes) // Instead, collect all attributes and inner nodes to write. //Items to be written as attributes are those that start with @ or are listed in AttributeFields part of XmlJsonOutputSettings. var innerNodesToWrite = new List <Tuple <string, object> >(); foreach (var item in line.Items) { if (item.Item1[0] == '@') { //attribute attrsToWrite.Add(Tuple.Create(item.Item1.Substring(1), item.Item2)); } else if (_attributeFields.Contains(item.Item1)) { //attribute attrsToWrite.Add(item); } else { //inner node innerNodesToWrite.Add(item); } } attrsToWrite.ForEach(async a => await _xmlWriter.WriteAttributeStringAsync(null, a.Item1, null, a.Item2?.ToString())); //attributes (note that ToString() for string is just a reference to itself, so no perfromance penalty) innerNodesToWrite.ForEach(async n => await _xmlWriter.WriteElementStringAsync(null, n.Item1, null, n.Item2?.ToString())); //inner nodes } else { //we're not at the leaf level yet, write the attributes and recurse attrsToWrite.ForEach(async a => await _xmlWriter.WriteAttributeStringAsync(null, a.Item1, null, a.Item2?.ToString())); //attributes await WriteXrecordAsync(tail, line); } await _xmlWriter.WriteEndElementAsync(); //end element (note that it will skip full end tag in case of no inner nodes (WriteFullEndElementAsync can be used to avoid this) }
public void ctor_ComplexSpecs_CorrectData() { //arrange var actual = new XmlNodePath("./FirstNode[@id=1]//SecondNode"); //act var nodeDefs = actual.NodeDefs; //assert nodeDefs.Count.Should().Be(2); var nodeDef = nodeDefs[0]; nodeDef.Name.Should().Be("FirstNode"); var attrs = nodeDef.GetAttributes(); attrs.Count.Should().Be(1); attrs[0].Item1.Should().Be("id"); attrs[0].Item2.Should().Be("1"); nodeDef = nodeDefs[1]; nodeDef.Name.Should().Be("SecondNode"); nodeDef.GetAttributes().Count.Should().Be(0); }
private int _clstrBaseDepth, _recBaseDepth; //level limit when advancing to cluster/record node (i.e. level below collection/cluster node respectively) /// <summary> /// Create an instance of the record supplier from XML /// </summary> /// <param name="reader">Underlying text reader.</param> /// <param name="sourceNo"></param> /// <param name="settings"></param> /// <param name="intakeIsAsync"></param> internal XmlFeederForSource(TextReader reader, int sourceNo, string settings, bool intakeIsAsync) : base(reader, sourceNo) //Note that reader is passed to base class, even though it is not used there (all relevant methods are overridden, so null could've been passed instead). //This is because base.Dispose (called by Dispose in this class) disposes the reader (it is unclear whether XmlReader.Dispose disposes underlying reader). { _xmlReader = XmlReader.Create(reader, new XmlReaderSettings() { ConformanceLevel = ConformanceLevel.Fragment, IgnoreWhitespace = true, IgnoreComments = true, IgnoreProcessingInstructions = true, Async = intakeIsAsync }); var settingDict = settings?.SplitPairsToListOfTuples()?.ToDictionary(t => t.Item1, t => t.Item2); // Discrete settings to unpack: // CollectionNode - "xpath" to the collection of clusters/records (may be null/empty, in which case input stream contains XML fragment where each root constitutes record or cluster) // ClusterNode - "xpath" to cluster node within collection node (null/empty means single record clusters) // RecordNode - "xpath" to record node within cluster node (or collection node if cluster node is empty) // IncludeExplicitText - true to include explicit text in RecordNode; false (default) to ignore it // IncludeAttributes - true to include attributes (prefixed by @); truePlain to include attributes w/o prefix; false (default) to ignore them // "xpath" is always relative (no need for ./), each of the nodes (separated by /) can contain attribute _collNodePath = new XmlNodePath(settingDict.GetStringSetting("CollectionNode")); _clstrNodePath = new XmlNodePath(settingDict.GetStringSetting("ClusterNode")); _xrecNodePath = new XmlNodePath(settingDict.GetStringSetting("RecordNode")); _includeExplicitText = settingDict.GetStringSetting("IncludeExplicitText")?.ToLower() == "true"; var inclAttrsSetting = settingDict.GetStringSetting("IncludeAttributes")?.ToLower(); _includeAttributes = inclAttrsSetting.SafeSubstring(0, 4) == "true"; _addPrefixToAttrKeys = inclAttrsSetting != "trueplain"; _observeClusters = !_clstrNodePath.IsEmpty; _addClusterDataToTraceBin = settingDict.GetStringSetting("AddClusterDataToTraceBin")?.ToLower() == "true"; _currClstrCnt = 0; //will stay at 0 (undetermined) unless ClusterNode defined _readerState = ReaderState.BeforeCollection; _clstrBaseDepth = _recBaseDepth = -1; }
/// <summary> /// Asynchronously read _xmlReader until at given location. /// </summary> /// <param name="nodePath">Location path relative to current position.</param> /// <param name="baseDepth">Level (Depth) passed at the initial call to limit the scope of search for inner elements.</param> /// <param name="depth">Level (Depth) of the initial call this method (intended to only be passed during recursive calls).</param> /// <param name="initPath">Location path from the initial call this method (intended to only be passed during recursive calls - needed in case of reset).</param> /// <param name="firstInCluster">Indicator that the record is first in cluster (complicated/unreliable - to be refactored).</param> /// <param name="addAttrsToTraceBin">Indicator to add collected attribute data to trace bin; applicable only if AddClusterDataToTraceBin (which in turn is only applicable if _observeClusters).</param> /// <returns>Task with true if succeeded or false if location not found.</returns> private async Task <bool> AdvanceToLocationAsync(XmlNodePath nodePath, int baseDepth, int depth = -9, XmlNodePath initPath = null, bool firstInCluster = false, bool addAttrsToTraceBin = false) { Debug.Assert(!nodePath.IsEmpty); if (initPath == null) { initPath = nodePath; } var head = nodePath.NodeDefs[0]; //XmlNodeDef representing the first xpath fragment (before the first /), i.e. current node pattern var tail = nodePath.NodeDefs.Skip(1).Any() ? new XmlNodePath(nodePath.NodeDefs.Skip(1)) //XmlNodePath representing the xpath fragments after the first /, i.e. the remaining node patterns : null; //end of recursion do { } //advance till next element (ignore e.g. embedded text) while (await _xmlReader.ReadAsync() && !_xmlReader.IsStartElement() && _xmlReader.Depth > baseDepth); if (_xmlReader.EOF) { return(false); } //Distinction between initial level and base level : // initDepth - level at the beginning of initial (external) call (then passed with each recursive iteration) // baseDepth - level limit when advancing to next element (e.g. if advancing to cluster, it is the collection level) //Always: initDepth >= baseDepth (i.e. each call is made within level limit) int initDepth = depth == -9 ? _xmlReader.Depth : depth; //-9 means initial call from outside (as opposed to recursive call) if (MatchFound(head)) { if (addAttrsToTraceBin) { _traceBinKeyPrefix = CreateKey(_traceBinKeyPrefix); GetAttributes(_traceBinKeyPrefix, false).ToList().ForEach(t => _traceBin.Add(t.Item1, t.Item2)); } initDepth++; //are we there yet? if (tail == null || await AdvanceToLocationAsync(tail, baseDepth, initDepth, tail, false, addAttrsToTraceBin)) { return(true); //success! } } //no match here, attempt to go back to the initial level and start over do { if (_xmlReader.Depth < initDepth) { return(false); //beyond the initial level } if (_xmlReader.Depth == initDepth) { //at initial level, try again (or fail search if first in cluster) if (_xmlReader.NodeType == XmlNodeType.EndElement && !firstInCluster) { return(false); } //TODO: refactor (remove reliance on firstInCluster) - too complicated. return(await AdvanceToLocationAsync(initPath, baseDepth)); } } while (await _xmlReader.ReadAsync()); //end of stream, no xpath found return(false); }