Free cookie consent management tool by TermsFeed Policy Generator

source: branches/PersistenceReintegration/HeuristicLab.ExtLibs/HeuristicLab.NRefactory/5.5.0/NRefactory-5.5.0/Documentation/XmlDocumentationProvider.cs @ 15866

Last change on this file since 15866 was 11700, checked in by jkarder, 10 years ago

#2077: created branch and added first version

File size: 13.3 KB
Line 
1// Copyright (c) 2010-2013 AlphaSierraPapa for the SharpDevelop Team
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4// software and associated documentation files (the "Software"), to deal in the Software
5// without restriction, including without limitation the rights to use, copy, modify, merge,
6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7// to whom the Software is furnished to do so, subject to the following conditions:
8//
9// The above copyright notice and this permission notice shall be included in all copies or
10// substantial portions of the Software.
11//
12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17// DEALINGS IN THE SOFTWARE.
18
19using System;
20using System.Collections.Generic;
21using System.Diagnostics;
22using System.IO;
23using System.Runtime.Serialization;
24using System.Xml;
25using ICSharpCode.NRefactory.Editor;
26using ICSharpCode.NRefactory.TypeSystem;
27
28namespace ICSharpCode.NRefactory.Documentation
29{
30  /// <summary>
31  /// Provides documentation from an .xml file (as generated by the Microsoft C# compiler).
32  /// </summary>
33  /// <remarks>
34  /// This class first creates an in-memory index of the .xml file, and then uses that to read only the requested members.
35  /// This way, we avoid keeping all the documentation in memory.
36  /// The .xml file is only opened when necessary, the file handle is not kept open all the time.
37  /// If the .xml file is changed, the index will automatically be recreated.
38  /// </remarks>
39  [Serializable]
40  public class XmlDocumentationProvider : IDocumentationProvider, IDeserializationCallback
41  {
42    #region Cache
43    sealed class XmlDocumentationCache
44    {
45      readonly KeyValuePair<string, string>[] entries;
46      int pos;
47     
48      public XmlDocumentationCache(int size = 50)
49      {
50        if (size <= 0)
51          throw new ArgumentOutOfRangeException("size", size, "Value must be positive");
52        this.entries = new KeyValuePair<string, string>[size];
53      }
54     
55      internal bool TryGet(string key, out string value)
56      {
57        foreach (var pair in entries) {
58          if (pair.Key == key) {
59            value = pair.Value;
60            return true;
61          }
62        }
63        value = null;
64        return false;
65      }
66     
67      internal void Add(string key, string value)
68      {
69        entries[pos++] = new KeyValuePair<string, string>(key, value);
70        if (pos == entries.Length)
71          pos = 0;
72      }
73    }
74    #endregion
75   
76    [Serializable]
77    struct IndexEntry : IComparable<IndexEntry>
78    {
79      /// <summary>
80      /// Hash code of the documentation tag
81      /// </summary>
82      internal readonly int HashCode;
83     
84      /// <summary>
85      /// Position in the .xml file where the documentation starts
86      /// </summary>
87      internal readonly int PositionInFile;
88     
89      internal IndexEntry(int hashCode, int positionInFile)
90      {
91        this.HashCode = hashCode;
92        this.PositionInFile = positionInFile;
93      }
94     
95      public int CompareTo(IndexEntry other)
96      {
97        return this.HashCode.CompareTo(other.HashCode);
98      }
99    }
100   
101    [NonSerialized]
102    XmlDocumentationCache cache = new XmlDocumentationCache();
103   
104    readonly string fileName;
105    volatile IndexEntry[] index; // SORTED array of index entries
106   
107    #region Constructor / Redirection support
108    /// <summary>
109    /// Creates a new XmlDocumentationProvider.
110    /// </summary>
111    /// <param name="fileName">Name of the .xml file.</param>
112    /// <exception cref="IOException">Error reading from XML file (or from redirected file)</exception>
113    /// <exception cref="XmlException">Invalid XML file</exception>
114    public XmlDocumentationProvider(string fileName)
115    {
116      if (fileName == null)
117        throw new ArgumentNullException("fileName");
118     
119      using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
120        using (XmlTextReader xmlReader = new XmlTextReader(fs)) {
121          xmlReader.XmlResolver = null; // no DTD resolving
122          xmlReader.MoveToContent();
123          if (string.IsNullOrEmpty(xmlReader.GetAttribute("redirect"))) {
124            this.fileName = fileName;
125            ReadXmlDoc(xmlReader);
126          } else {
127            string redirectionTarget = GetRedirectionTarget(fileName, xmlReader.GetAttribute("redirect"));
128            if (redirectionTarget != null) {
129              Debug.WriteLine("XmlDoc " + fileName + " is redirecting to " + redirectionTarget);
130              using (FileStream redirectedFs = new FileStream(redirectionTarget, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
131                using (XmlTextReader redirectedXmlReader = new XmlTextReader(redirectedFs)) {
132                  redirectedXmlReader.XmlResolver = null; // no DTD resolving
133                  this.fileName = redirectionTarget;
134                  ReadXmlDoc(redirectedXmlReader);
135                }
136              }
137            } else {
138              throw new XmlException("XmlDoc " + fileName + " is redirecting to " + xmlReader.GetAttribute("redirect") + ", but that file was not found.");
139            }
140          }
141        }
142      }
143    }
144   
145    static string GetRedirectionTarget(string xmlFileName, string target)
146    {
147      string programFilesDir = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
148      programFilesDir = AppendDirectorySeparator(programFilesDir);
149     
150      string corSysDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();
151      corSysDir = AppendDirectorySeparator(corSysDir);
152     
153      var fileName = target.Replace ("%PROGRAMFILESDIR%", programFilesDir)
154        .Replace ("%CORSYSDIR%", corSysDir);
155      if (!Path.IsPathRooted (fileName))
156        fileName = Path.Combine (Path.GetDirectoryName (xmlFileName), fileName);
157      return LookupLocalizedXmlDoc(fileName);
158    }
159   
160    static string AppendDirectorySeparator(string dir)
161    {
162      if (dir.EndsWith("\\", StringComparison.Ordinal) || dir.EndsWith("/", StringComparison.Ordinal))
163        return dir;
164      else
165        return dir + Path.DirectorySeparatorChar;
166    }
167   
168    /// <summary>
169    /// Given the assembly file name, looks up the XML documentation file name.
170    /// Returns null if no XML documentation file is found.
171    /// </summary>
172    public static string LookupLocalizedXmlDoc(string fileName)
173    {
174      string xmlFileName = Path.ChangeExtension(fileName, ".xml");
175      string currentCulture = System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
176      string localizedXmlDocFile = GetLocalizedName(xmlFileName, currentCulture);
177     
178      Debug.WriteLine("Try find XMLDoc @" + localizedXmlDocFile);
179      if (File.Exists(localizedXmlDocFile)) {
180        return localizedXmlDocFile;
181      }
182      Debug.WriteLine("Try find XMLDoc @" + xmlFileName);
183      if (File.Exists(xmlFileName)) {
184        return xmlFileName;
185      }
186      if (currentCulture != "en") {
187        string englishXmlDocFile = GetLocalizedName(xmlFileName, "en");
188        Debug.WriteLine("Try find XMLDoc @" + englishXmlDocFile);
189        if (File.Exists(englishXmlDocFile)) {
190          return englishXmlDocFile;
191        }
192      }
193      return null;
194    }
195   
196    static string GetLocalizedName(string fileName, string language)
197    {
198      string localizedXmlDocFile = Path.GetDirectoryName(fileName);
199      localizedXmlDocFile = Path.Combine(localizedXmlDocFile, language);
200      localizedXmlDocFile = Path.Combine(localizedXmlDocFile, Path.GetFileName(fileName));
201      return localizedXmlDocFile;
202    }
203    #endregion
204   
205    #region Load / Create Index
206    void ReadXmlDoc(XmlTextReader reader)
207    {
208      //lastWriteDate = File.GetLastWriteTimeUtc(fileName);
209      // Open up a second file stream for the line<->position mapping
210      using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
211        LinePositionMapper linePosMapper = new LinePositionMapper(fs);
212        List<IndexEntry> indexList = new List<IndexEntry>();
213        while (reader.Read()) {
214          if (reader.IsStartElement()) {
215            switch (reader.LocalName) {
216              case "members":
217                ReadMembersSection(reader, linePosMapper, indexList);
218                break;
219            }
220          }
221        }
222        indexList.Sort();
223        this.index = indexList.ToArray(); // volatile write
224      }
225    }
226   
227    sealed class LinePositionMapper
228    {
229      readonly FileStream fs;
230      int currentLine = 1;
231     
232      public LinePositionMapper(FileStream fs)
233      {
234        this.fs = fs;
235      }
236     
237      public int GetPositionForLine(int line)
238      {
239        Debug.Assert(line >= currentLine);
240        while (line > currentLine) {
241          int b = fs.ReadByte();
242          if (b < 0)
243            throw new EndOfStreamException();
244          if (b == '\n') {
245            currentLine++;
246          }
247        }
248        return checked((int)fs.Position);
249      }
250    }
251   
252    static void ReadMembersSection(XmlTextReader reader, LinePositionMapper linePosMapper, List<IndexEntry> indexList)
253    {
254      while (reader.Read()) {
255        switch (reader.NodeType) {
256          case XmlNodeType.EndElement:
257            if (reader.LocalName == "members") {
258              return;
259            }
260            break;
261          case XmlNodeType.Element:
262            if (reader.LocalName == "member") {
263              int pos = linePosMapper.GetPositionForLine(reader.LineNumber) + Math.Max(reader.LinePosition - 2, 0);
264              string memberAttr = reader.GetAttribute("name");
265              if (memberAttr != null)
266                indexList.Add(new IndexEntry(GetHashCode(memberAttr), pos));
267              reader.Skip();
268            }
269            break;
270        }
271      }
272    }
273   
274    /// <summary>
275    /// Hash algorithm used for the index.
276    /// This is a custom implementation so that old index files work correctly
277    /// even when the .NET string.GetHashCode implementation changes
278    /// (e.g. due to .NET 4.5 hash randomization)
279    /// </summary>
280    static int GetHashCode(string key)
281    {
282      unchecked {
283        int h = 0;
284        foreach (char c in key) {
285          h = (h << 5) - h + c;
286        }
287        return h;
288      }
289    }
290    #endregion
291   
292    #region GetDocumentation
293    /// <summary>
294    /// Get the documentation for the member with the specified documentation key.
295    /// </summary>
296    public string GetDocumentation(string key)
297    {
298      if (key == null)
299        throw new ArgumentNullException("key");
300      return GetDocumentation(key, true);
301    }
302   
303    string GetDocumentation(string key, bool allowReload)
304    {
305      int hashcode = GetHashCode(key);
306      var index = this.index; // read volatile field
307      // index is sorted, so we can use binary search
308      int m = Array.BinarySearch(index, new IndexEntry(hashcode, 0));
309      if (m < 0)
310        return null;
311      // correct hash code found.
312      // possibly there are multiple items with the same hash, so go to the first.
313      while (--m >= 0 && index[m].HashCode == hashcode);
314      // m is now 1 before the first item with the correct hash
315     
316      XmlDocumentationCache cache = this.cache;
317      lock (cache) {
318        string val;
319        if (!cache.TryGet(key, out val)) {
320          try {
321            // go through all items that have the correct hash
322            while (++m < index.Length && index[m].HashCode == hashcode) {
323              val = LoadDocumentation(key, index[m].PositionInFile);
324              if (val != null)
325                break;
326            }
327            // cache the result (even if it is null)
328            cache.Add(key, val);
329          } catch (IOException) {
330            // may happen if the documentation file was deleted/is inaccessible/changed (EndOfStreamException)
331            return allowReload ? ReloadAndGetDocumentation(key) : null;
332          } catch (XmlException) {
333            // may happen if the documentation file was changed so that the file position no longer starts on a valid XML element
334            return allowReload ? ReloadAndGetDocumentation(key) : null;
335          }
336        }
337        return val;
338      }
339    }
340   
341    string ReloadAndGetDocumentation(string key)
342    {
343      try {
344        // Reload the index
345        using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
346          using (XmlTextReader xmlReader = new XmlTextReader(fs)) {
347            xmlReader.XmlResolver = null; // no DTD resolving
348            xmlReader.MoveToContent();
349            ReadXmlDoc(xmlReader);
350          }
351        }
352      } catch (IOException) {
353        // Ignore errors on reload; IEntity.Documentation callers aren't prepared to handle exceptions
354        this.index = new IndexEntry[0]; // clear index to avoid future load attempts
355        return null;
356      } catch (XmlException) {
357        this.index = new IndexEntry[0]; // clear index to avoid future load attempts
358        return null;       
359      }
360      return GetDocumentation(key, allowReload: false); // prevent infinite reload loops
361    }
362    #endregion
363   
364    #region GetDocumentation for entity
365    /// <inheritdoc/>
366    public DocumentationComment GetDocumentation(IEntity entity)
367    {
368      string xmlDoc = GetDocumentation(IdStringProvider.GetIdString(entity));
369      if (xmlDoc != null) {
370        return new DocumentationComment(new StringTextSource(xmlDoc), new SimpleTypeResolveContext(entity));
371      } else {
372        return null;
373      }
374    }
375    #endregion
376   
377    #region Load / Read XML
378    string LoadDocumentation(string key, int positionInFile)
379    {
380      using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
381        fs.Position = positionInFile;
382        using (XmlTextReader r = new XmlTextReader(fs, XmlNodeType.Element, null)) {
383          r.XmlResolver = null; // no DTD resolving
384          while (r.Read()) {
385            if (r.NodeType == XmlNodeType.Element) {
386              string memberAttr = r.GetAttribute("name");
387              if (memberAttr == key) {
388                return r.ReadInnerXml();
389              } else {
390                return null;
391              }
392            }
393          }
394          return null;
395        }
396      }
397    }
398    #endregion
399   
400    public virtual void OnDeserialization(object sender)
401    {
402      cache = new XmlDocumentationCache();
403    }
404  }
405}
Note: See TracBrowser for help on using the repository browser.