File: System\Activities\Debugger\XmlReaderWithSourceLocation.cs
Project: ndp\cdf\src\NetFx40\System.Activities\System.Activities.csproj (System.Activities)
// <copyright>
//   Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
 
namespace System.Activities.Debugger
{
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Xml;
 
    internal class XmlReaderWithSourceLocation : XmlWrappingReader
    {
        private Dictionary<DocumentLocation, DocumentRange> attributeValueRanges;
        private Dictionary<DocumentLocation, DocumentRange> emptyElementRanges;
        private Dictionary<DocumentLocation, DocumentRange> contentValueRanges;
        private Dictionary<DocumentLocation, DocumentLocation> startElementLocations;
        private Dictionary<DocumentLocation, DocumentLocation> endElementLocations;
        private CharacterSpottingTextReader characterSpottingTextReader;
        private Stack<DocumentLocation> contentStartLocationStack;
 
        [SuppressMessage("Microsoft.Security.Xml", "CA3053:UseXmlSecureResolver", 
            Justification = @"For the call to XmlReader.Create() below, CA3053 recommends setting the 
XmlReaderSettings.XmlResolver property to either null or an instance of XmlSecureResolver. 
But after setting this property to null, a warning of CA3053 still shows up in FxCop. 
So we suppress this error until the reporting for CA3053 has been updated to fix this issue.")]
        public XmlReaderWithSourceLocation(TextReader underlyingTextReader)
        {
            UnitTestUtility.Assert(underlyingTextReader != null, "CharacterSpottingTextReader cannot be null and should be ensured by caller.");
            CharacterSpottingTextReader characterSpottingTextReader = new CharacterSpottingTextReader(underlyingTextReader);
            this.BaseReader = XmlReader.Create(characterSpottingTextReader, new XmlReaderSettings { XmlResolver = null });
            UnitTestUtility.Assert(this.BaseReaderAsLineInfo != null, "The XmlReader created by XmlReader.Create should ensure this.");
            UnitTestUtility.Assert(this.BaseReaderAsLineInfo.HasLineInfo(), "The XmlReader created by XmlReader.Create should ensure this.");
            this.characterSpottingTextReader = characterSpottingTextReader;
            this.contentStartLocationStack = new Stack<DocumentLocation>();
        }
 
        public Dictionary<DocumentLocation, DocumentRange> AttributeValueRanges
        {
            get
            {
                if (this.attributeValueRanges == null)
                {
                    this.attributeValueRanges = new Dictionary<DocumentLocation, DocumentRange>();
                }
 
                return this.attributeValueRanges;
            }
        }
 
        public Dictionary<DocumentLocation, DocumentRange> ContentValueRanges
        {
            get
            {
                if (this.contentValueRanges == null)
                {
                    this.contentValueRanges = new Dictionary<DocumentLocation, DocumentRange>();
                }
 
                return this.contentValueRanges;
            }
        }
 
        public Dictionary<DocumentLocation, DocumentRange> EmptyElementRanges
        {
            get
            {
                if (this.emptyElementRanges == null)
                {
                    this.emptyElementRanges = new Dictionary<DocumentLocation, DocumentRange>();
                }
 
                return this.emptyElementRanges;
            }
        }
 
        public Dictionary<DocumentLocation, DocumentLocation> StartElementLocations
        {
            get
            {
                if (this.startElementLocations == null)
                {
                    this.startElementLocations = new Dictionary<DocumentLocation, DocumentLocation>();
                }
 
                return this.startElementLocations;
            }
        }
 
        public Dictionary<DocumentLocation, DocumentLocation> EndElementLocations
        {
            get
            {
                if (this.endElementLocations == null)
                {
                    this.endElementLocations = new Dictionary<DocumentLocation, DocumentLocation>();
                }
 
                return this.endElementLocations;
            }
        }
 
        private DocumentLocation CurrentLocation
        {
            get
            {
                return new DocumentLocation(this.BaseReaderAsLineInfo.LineNumber, this.BaseReaderAsLineInfo.LinePosition);
            }
        }
 
        public override bool Read()
        {
            bool result = base.Read();
            if (this.NodeType == Xml.XmlNodeType.Element)
            {
                DocumentLocation elementLocation = this.CurrentLocation;
                if (this.IsEmptyElement)
                {
                    DocumentRange emptyElementRange = this.FindEmptyElementRange(elementLocation);
                    this.EmptyElementRanges.Add(elementLocation, emptyElementRange);
                }
                else
                {
                    DocumentLocation startElementBracket = this.FindStartElementBracket(elementLocation);
                    this.StartElementLocations.Add(elementLocation, startElementBracket);
 
                    // Push a null as a place holder. In XmlNodeType.Text part, we replace this
                    // null with real data. Why not pushing real data only without this place holder?
                    // Because in XmlNodeType.EndElement, we need to know whether there is Text. Think 
                    // about situation like <a>Text1<b><c>Text2</c></b>Text3</a>
                    // So, each time an Element starts, we push a place holder in the stack so that Start
                    // and End don't mis-match.
                    this.contentStartLocationStack.Push(null);
                }
 
                int attributeCount = this.AttributeCount;
                if (attributeCount > 0)
                {
                    for (int i = 0; i < attributeCount; i++)
                    {
                        this.MoveToAttribute(i);
                        DocumentLocation memberLocation = this.CurrentLocation;
                        DocumentRange attributeValueRange = this.FindAttributeValueLocation(memberLocation);
                        this.AttributeValueRanges.Add(memberLocation, attributeValueRange);
                    }
 
                    this.MoveToElement();
                }
            }
            else if (this.NodeType == Xml.XmlNodeType.EndElement)
            {
                DocumentLocation endElementLocation = this.CurrentLocation;
                DocumentLocation endElementBracket = this.FindEndElementBracket(endElementLocation);
                this.EndElementLocations.Add(endElementLocation, endElementBracket);
                UnitTestUtility.Assert(
                    this.contentStartLocationStack.Count > 0, 
                    "The stack should contain at least a null we pushed in StartElement.");
                DocumentLocation contentStartLocation = this.contentStartLocationStack.Pop();
                if (contentStartLocation != null)
                {
                    DocumentLocation contentEnd = this.FindContentEndBefore(endElementLocation);
                    this.ContentValueRanges.Add(endElementLocation, new DocumentRange(contentStartLocation, contentEnd));
                }
            }
            else if (this.NodeType == Xml.XmlNodeType.Text)
            {
                UnitTestUtility.Assert(this.contentStartLocationStack.Count > 0, "Adding Text with out StartElement?");
                if (this.contentStartLocationStack.Peek() == null)
                { 
                    // no text was added since the last StartElement.
                    // This is the start of the content of this Element.
                    // <a>ABCDE</a>
                    // Sometimes, xml reader gives the text by ABC and DE in 
                    // two times.
                    this.contentStartLocationStack.Pop();
                    this.contentStartLocationStack.Push(this.CurrentLocation);    
                }
            }
 
            return result;
        }
 
        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            if (disposing)
            {
                if (this.characterSpottingTextReader != null)
                {
                    ((IDisposable)this.characterSpottingTextReader).Dispose();
                }
 
                this.characterSpottingTextReader = null;
            }
        }
 
        private DocumentLocation FindStartElementBracket(DocumentLocation elementLocation)
        {
            return this.characterSpottingTextReader.FindCharacterStrictlyBefore('<', elementLocation);
        }
 
        private DocumentLocation FindEndElementBracket(DocumentLocation elementLocation)
        {
            return this.characterSpottingTextReader.FindCharacterStrictlyAfter('>', elementLocation);
        }
 
        private DocumentRange FindEmptyElementRange(DocumentLocation elementLocation)
        {
            DocumentLocation startBracket = this.FindStartElementBracket(elementLocation);
            DocumentLocation endBracket = this.FindEndElementBracket(elementLocation);
            UnitTestUtility.Assert(startBracket != null, "XmlReader should guarantee there must be a start angle bracket.");
            UnitTestUtility.Assert(endBracket != null, "XmlReader should guarantee there must be an end angle bracket.");
            DocumentRange emptyElementRange = new DocumentRange(startBracket, endBracket);
            return emptyElementRange;
        }
 
        private DocumentRange FindAttributeValueLocation(DocumentLocation memberLocation)
        {
            UnitTestUtility.Assert(this.characterSpottingTextReader != null, "Ensured by constructor.");
            DocumentLocation attributeStart = this.characterSpottingTextReader.FindCharacterStrictlyAfter(this.QuoteChar, memberLocation);
            UnitTestUtility.Assert(attributeStart != null, "Read should ensure the two quote characters exists");
            DocumentLocation attributeEnd = this.characterSpottingTextReader.FindCharacterStrictlyAfter(this.QuoteChar, attributeStart);
            UnitTestUtility.Assert(attributeEnd != null, "Read should ensure the two quote characters exists");
            return new DocumentRange(attributeStart, attributeEnd);
        }
 
        private DocumentLocation FindContentEndBefore(DocumentLocation location)
        {
            DocumentLocation contentEnd = this.FindStartElementBracket(location);
            int linePosition = contentEnd.LinePosition.Value - 1;
 
            // Line position is 1-based
            if (linePosition < 1)
            {
                return this.characterSpottingTextReader.FindCharacterStrictlyBefore('\n', contentEnd);
            }
            else
            {
                return new DocumentLocation(contentEnd.LineNumber.Value, linePosition);
            }
        }
    }
}