// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Xml
{
/// <summary>
/// Abstact base class for all types
/// </summary>
public abstract class AXmlObject: TextSegment
{
/// <summary> Empty string. The namespace used if there is no "xmlns" specified </summary>
public static readonly string NoNamespace = string.Empty;
/// <summary> Namespace for "xml:" prefix: "http://www.w3.org/XML/1998/namespace" </summary>
public static readonly string XmlNamespace = "http://www.w3.org/XML/1998/namespace";
/// <summary> Namesapce for "xmlns:" prefix: "http://www.w3.org/2000/xmlns/" </summary>
public static readonly string XmlnsNamespace = "http://www.w3.org/2000/xmlns/";
/// <summary> Parent node. </summary>
/// <remarks>
/// New cached items start with null parent.
/// Cache constraint:
/// If cached item has parent set, then the whole subtree must be consistent
/// </remarks>
public AXmlObject Parent { get; set; }
/// <summary>
/// Gets the document that has owns this object.
/// Once set, it is not changed. Not even set to null.
/// </summary>
internal AXmlDocument Document { get; set; }
/// <summary> Creates new object </summary>
protected AXmlObject()
{
this.LastUpdatedFrom = this;
}
/// <summary> Occurs before the value of any local properties changes. Nested changes do not cause the event to occur </summary>
public event EventHandler<AXmlObjectEventArgs> Changing;
/// <summary> Occurs after the value of any local properties changed. Nested changes do not cause the event to occur </summary>
public event EventHandler<AXmlObjectEventArgs> Changed;
/// <summary> Raises Changing event </summary>
protected void OnChanging()
{
AXmlParser.Log("Changing {0}", this);
if (Changing != null) {
Changing(this, new AXmlObjectEventArgs() { Object = this } );
}
AXmlDocument doc = this.Document;
if (doc != null) {
doc.OnObjectChanging(this);
}
// As a convenience, also rasie an event for the parent element
AXmlTag me = this as AXmlTag;
if (me != null && (me.IsStartOrEmptyTag || me.IsEndTag) && me.Parent is AXmlElement) {
me.Parent.OnChanging();
}
}
/// <summary> Raises Changed event </summary>
protected void OnChanged()
{
AXmlParser.Log("Changed {0}", this);
if (Changed != null) {
Changed(this, new AXmlObjectEventArgs() { Object = this } );
}
AXmlDocument doc = this.Document;
if (doc != null) {
doc.OnObjectChanged(this);
}
// As a convenience, also rasie an event for the parent element
AXmlTag me = this as AXmlTag;
if (me != null && (me.IsStartOrEmptyTag || me.IsEndTag) && me.Parent is AXmlElement) {
me.Parent.OnChanged();
}
}
List<SyntaxError> syntaxErrors;
/// <summary>
/// The error that occured in the context of this node (excluding nested nodes)
/// </summary>
public IEnumerable<SyntaxError> MySyntaxErrors {
get {
if (syntaxErrors == null) {
return new SyntaxError[] {};
} else {
return syntaxErrors;
}
}
}
/// <summary>
/// The error that occured in the context of this node and all nested nodes.
/// It has O(n) cost.
/// </summary>
public IEnumerable<SyntaxError> SyntaxErrors {
get {
return GetSelfAndAllChildren().SelectMany(obj => obj.MySyntaxErrors);
}
}
internal void AddSyntaxError(SyntaxError error)
{
DebugAssert(error.Object == this, "Must own the error");
if (this.syntaxErrors == null) this.syntaxErrors = new List<SyntaxError>();
syntaxErrors.Add(error);
}
/// <summary> Throws exception if condition is false </summary>
/// <remarks> Present in release mode - use only for very cheap aserts </remarks>
protected static void Assert(bool condition, string message)
{
if (!condition) {
throw new InternalException("Assertion failed: " + message);
}
}
/// <summary> Throws exception if condition is false </summary>
[Conditional("DEBUG")]
protected static void DebugAssert(bool condition, string message)
{
if (!condition) {
throw new InternalException("Assertion failed: " + message);
}
}
/// <summary> Recursively gets self and all nested nodes. </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate",
Justification = "Using a method makes the API look more LINQ-like and indicates that the returned collection is computed every time.")]
public virtual IEnumerable<AXmlObject> GetSelfAndAllChildren()
{
return new AXmlObject[] { this };
}
/// <summary> Get all ancestors of this node </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate",
Justification = "Using a method makes the API look more LINQ-like and indicates that the returned collection is computed every time.")]
public IEnumerable<AXmlObject> GetAncestors()
{
AXmlObject curr = this.Parent;
while(curr != null) {
yield return curr;
curr = curr.Parent;
}
}
/// <summary> Call appropriate visit method on the given visitor </summary>
public abstract void AcceptVisitor(IAXmlVisitor visitor);
/// <summary> The parser tree object this object was updated from </summary>
/// <remarks> Initialized to 'this' </remarks>
internal AXmlObject LastUpdatedFrom { get; private set; }
internal bool IsCached { get; set; }
/// <summary> Is call to UpdateDataFrom is allowed? </summary>
internal bool CanUpdateDataFrom(AXmlObject source)
{
return
this.GetType() == source.GetType() &&
this.StartOffset == source.StartOffset &&
(this.LastUpdatedFrom == source || !this.IsCached);
}
/// <summary> Copy all data from the 'source' to this object </summary>
/// <remarks> Returns true if any updates were done </remarks>
internal virtual bool UpdateDataFrom(AXmlObject source)
{
Assert(this.GetType() == source.GetType(), "Source has different type");
DebugAssert(this.StartOffset == source.StartOffset, "Source has different StartOffset");
if (this.LastUpdatedFrom == source) {
DebugAssert(this.EndOffset == source.EndOffset, "Source has different EndOffset");
return false;
}
Assert(!this.IsCached, "Can not update cached item");
Assert(source.IsCached, "Must update from cache");
this.LastUpdatedFrom = source;
this.StartOffset = source.StartOffset;
// In some cases we are just updating objects of that same
// type and position and hoping to be luckily right
this.EndOffset = source.EndOffset;
// Do not bother comparing - assume changed if non-null
if (this.syntaxErrors != null || source.syntaxErrors != null) {
// May be called again in derived class - oh, well, does not matter
OnChanging();
this.Document.Parser.TrackedSegments.RemoveSyntaxErrorsOf(this);
if (source.syntaxErrors == null) {
this.syntaxErrors = null;
} else {
this.syntaxErrors = new List<SyntaxError>();
foreach(var error in source.MySyntaxErrors) {
// The object differs, so create our own copy
// The source still might need it in the future and we do not want to break it
this.AddSyntaxError(error.Clone(this));
}
}
this.Document.Parser.TrackedSegments.AddSyntaxErrorsOf(this);
OnChanged();
}
return true;
}
/// <summary> Verify that the item is consistent. Only in debug build. </summary>
[Conditional("DEBUG")]
internal virtual void DebugCheckConsistency(bool allowNullParent)
{
}
/// <inheritdoc/>
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0}({1}-{2})", this.GetType().Name.Remove(0, 4), this.StartOffset, this.EndOffset);
}
#region Helpper methods
/// <summary> The part of name before ":" </summary>
/// <returns> Empty string if not found </returns>
protected static string GetNamespacePrefix(string name)
{
if (string.IsNullOrEmpty(name)) return string.Empty;
int colonIndex = name.IndexOf(':');
if (colonIndex != -1) {
return name.Substring(0, colonIndex);
} else {
return string.Empty;
}
}
/// <summary> The part of name after ":" </summary>
/// <returns> Whole name if ":" not found </returns>
protected static string GetLocalName(string name)
{
if (string.IsNullOrEmpty(name)) return string.Empty;
int colonIndex = name.IndexOf(':');
if (colonIndex != -1) {
return name.Remove(0, colonIndex + 1);
} else {
return name ?? string.Empty;
}
}
#endregion
}
}