// NAnt - A .NET build tool
// Copyright (C) 2001-2002 Gerry Shaw
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// Ian MacLean (imaclean@gmail.com)
// Gerry Shaw (gerry_shaw@yahoo.com)
using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.XPath;
using NAnt.Core.Util;
namespace NAnt.Core {
///
/// Maps XML nodes to the text positions from their original source.
///
[Serializable()]
public class LocationMap {
#region Private Instance Fields
// The LocationMap uses a hash table to map filenames to resolve specific maps.
private Hashtable _fileMap = new Hashtable();
#endregion Private Instance Fields
#region Public Instance Constructors
///
/// Initializes a new instance of the class.
///
public LocationMap() {
}
#endregion Public Instance Constructors
#region Public Instance Methods
///
/// Determines if a file has been loaded by the current project.
///
/// The file to check.
///
/// if the specified file has already been loaded
/// by the current project; otherwise, .
///
public bool FileIsMapped(string fileOrUri){
Uri uri = new Uri(fileOrUri);
return _fileMap.ContainsKey(uri.AbsoluteUri);
}
///
/// Adds an to the map.
///
///
/// An can only be added to the map once.
///
public void Add(XmlDocument doc) {
// check for non-backed documents
if (StringUtils.IsNullOrEmpty(doc.BaseURI)) {
return;
}
// convert URI to absolute URI
Uri uri = new Uri(doc.BaseURI);
string fileName = uri.AbsoluteUri;
// prevent duplicate mapping
if (FileIsMapped(fileName)) {
// do not re-map the file a 2nd time
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "XML document '{0}' has already been mapped.", fileName));
}
Hashtable map = new Hashtable();
string parentXPath = "/"; // default to root
string previousXPath = "";
int previousDepth = 0;
// Load text reader.
XmlTextReader reader = new XmlTextReader(fileName);
try {
map.Add((object) "/", (object) new TextPosition(1, 1));
ArrayList indexAtDepth = new ArrayList();
// loop thru all nodes in the document
while (reader.Read()) {
// Ignore nodes we aren't interested in
if ((reader.NodeType != XmlNodeType.Whitespace) &&
(reader.NodeType != XmlNodeType.EndElement) &&
(reader.NodeType != XmlNodeType.ProcessingInstruction) &&
(reader.NodeType != XmlNodeType.XmlDeclaration)) {
int level = reader.Depth;
string currentXPath = "";
// If we are higher than before
if (reader.Depth < previousDepth) {
// Clear vars for new depth
string[] list = parentXPath.Split('/');
string newXPath = ""; // once appended to / will be root node ...
for (int j = 1; j < level+1; j++) {
newXPath += "/" + list[j];
}
// higher than before so trim xpath\
parentXPath = newXPath; // one up from before
// clear indexes for depth greater than ours
indexAtDepth.RemoveRange(level+1, indexAtDepth.Count - (level+1));
} else if (reader.Depth > previousDepth) {
// we are lower
parentXPath = previousXPath;
}
// End depth setup
// Setup up index array
// add any needed extra items ( usually only 1 )
// would have used array but not sure what maximum depth will be beforehand
for (int index = indexAtDepth.Count; index < level+1; index++) {
indexAtDepth.Add(0);
}
// Set child index
if ((int) indexAtDepth[level] == 0) {
// first time thru
indexAtDepth[level] = 1;
} else {
indexAtDepth[level] = (int) indexAtDepth[level] + 1; // lower so append to xpath
}
// Do actual XPath generation
if (parentXPath.EndsWith("/")) {
currentXPath = parentXPath;
} else {
currentXPath = parentXPath + "/"; // add seperator
}
// Set the final XPath
currentXPath += "child::node()[" + indexAtDepth[level] + "]";
// Add to our hash structures
map.Add((object) currentXPath, (object) new TextPosition(reader.LineNumber, reader.LinePosition));
// setup up loop vars for next iteration
previousXPath = currentXPath;
previousDepth = reader.Depth;
}
}
} finally {
reader.Close();
}
// add map at the end to prevent adding maps that had errors
_fileMap.Add(fileName, map);
}
///
/// Returns the in the XML file for the given node.
///
///
/// The must be from an
/// that has been added to the map.
///
public Location GetLocation(XmlNode node) {
// check for non-backed documents
if (StringUtils.IsNullOrEmpty(node.BaseURI)) {
return new Location(null, 0, 0 ); // return null location because we have a fileless node.
}
// convert URI to absolute URI
Uri uri = new Uri(node.BaseURI);
string fileName = uri.AbsoluteUri;
if (!FileIsMapped(fileName)) {
throw new ArgumentException("Xml node has not been mapped.");
}
// find xpath for node
Hashtable map = (Hashtable) _fileMap[fileName];
string xpath = GetXPathFromNode(node);
if (!map.ContainsKey(xpath)) {
throw new ArgumentException("Xml node has not been mapped.");
}
TextPosition pos = (TextPosition) map[xpath];
Location location = new Location(fileName, pos.Line, pos.Column);
return location;
}
#endregion Public Instance Methods
#region Private Instance Methods
private string GetXPathFromNode(XmlNode node) {
// IM TODO review this algorithm - tidy up
XPathNavigator nav = node.CreateNavigator();
string xpath = "";
int index = 0;
while (nav.NodeType.ToString(CultureInfo.InvariantCulture) != "Root") {
// loop thru children until we find ourselves
XPathNavigator navParent = nav.Clone();
navParent.MoveToParent();
int parentIndex = 0;
navParent.MoveToFirstChild();
if (navParent.IsSamePosition(nav)) {
index = parentIndex;
}
while (navParent.MoveToNext()) {
parentIndex++;
if (navParent.IsSamePosition(nav)) {
index = parentIndex;
}
}
nav.MoveToParent(); // do loop condition here
index++; // Convert to 1 based index
string thisNode = "child::node()[" + index.ToString(CultureInfo.InvariantCulture) + "]";
if (xpath.Length == 0) {
xpath = thisNode;
} else {
// build xpath string
xpath = thisNode + "/" + xpath;
}
}
// prepend slash to ...
xpath = "/" + xpath;
return xpath;
}
#endregion Private Instance Methods
///
/// Represents a position in the build file.
///
[Serializable()]
private struct TextPosition {
#region Public Instance Constructors
///
/// Initializes a new instance of the
/// with the speified line and column.
///
/// The line coordinate of the position.
/// The column coordinate of the position.
public TextPosition(int line, int column) {
Line = line;
Column = column;
}
#endregion Public Instance Constructors
#region Public Instance Fields
///
/// The line coordinate of the position.
///
public int Line;
///
/// The column coordinate of the position.
///
public int Column;
#endregion Public Instance Fields
}
}
}