[monodoc] Re-add now obsolete ArrayList-based Node property and use ChildNodes as...
[mono.git] / mcs / class / monodoc / Monodoc / RootTree.cs
1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using System.Collections.Specialized;
5 using System.Configuration;
6 using System.IO;
7 using System.Linq;
8 using System.Reflection;
9 using System.Runtime.InteropServices;
10 using System.Xml;
11
12 using Monodoc.Providers;
13 using Lucene.Net.Analysis.Standard;
14 using Lucene.Net.Index;
15
16 namespace Monodoc
17 {
18         public
19 #if LEGACY_MODE
20         partial
21 #endif
22         class RootTree : Tree
23         {
24                 public const int MonodocVersion = 2;
25                 const string RootNamespace = "root:/";
26                 string basedir;
27                 static List<string> uncompiledHelpSourcePaths = new List<string>();
28                 HashSet<string> loadedSourceFiles = new HashSet<string>();
29                 List<HelpSource> helpSources = new List<HelpSource>();
30                 Dictionary<string, Node> nameToNode = new Dictionary<string, Node>();
31                 Dictionary<string, HelpSource> nameToHelpSource = new Dictionary<string, HelpSource>();
32
33                 public IList<HelpSource> HelpSources {
34                         get {
35                                 return this.helpSources.AsReadOnly();
36                         }
37                 }
38
39                 public DateTime LastHelpSourceTime {
40                         get;
41                         set;
42                 }
43
44                 static bool IsUnix {
45                         get {
46                                 int platform = (int)Environment.OSVersion.Platform;
47                                 return platform == 4 || platform == 128 || platform == 6;
48                         }
49                 }
50
51                 RootTree () : base (null, "Mono Documentation", "root:")
52                 {
53                         base.RootNode.EnsureNodes();
54                         this.LastHelpSourceTime = DateTime.Now;
55                 }
56
57                 public static void AddUncompiledSource (string path)
58                 {
59                         uncompiledHelpSourcePaths.Add (path);
60                 }
61
62                 public static RootTree LoadTree ()
63                 {
64                         return RootTree.LoadTree (RootTree.ProbeBaseDirectories ());
65                 }
66
67                 static string ProbeBaseDirectories ()
68                 {
69                         string result = ".";
70                         try {
71                                 result = Config.Get ("docPath") ?? ".";
72                         } catch {}
73
74                         return result;
75                 }
76
77                 public static RootTree LoadTree (string basedir, bool includeExternal = true)
78                 {
79                         if (string.IsNullOrEmpty (basedir))
80                                 throw new ArgumentNullException ("basedir");
81                         if (!Directory.Exists (basedir))
82                                 throw new ArgumentException ("basedir", string.Format ("Base documentation directory at '{0}' doesn't exist", basedir));
83
84                         XmlDocument xmlDocument = new XmlDocument ();
85                         string filename = Path.Combine (basedir, "monodoc.xml");
86                         xmlDocument.Load (filename);
87                         IEnumerable<string> sourceFiles = Directory.EnumerateFiles (Path.Combine (basedir, "sources"), "*.source");
88                         if (includeExternal)
89                                 sourceFiles = sourceFiles.Concat (RootTree.ProbeExternalDirectorySources ());
90                         return RootTree.LoadTree (basedir, xmlDocument, sourceFiles);
91                 }
92
93                 static IEnumerable<string> ProbeExternalDirectorySources ()
94                 {
95                         IEnumerable<string> enumerable = Enumerable.Empty<string> ();
96                         try {
97                                 string path = Config.Get ("docExternalPath");
98                                 enumerable = enumerable.Concat (System.IO.Directory.EnumerateFiles (path, "*.source"));
99                         }
100                         catch {}
101
102                         if (Directory.Exists ("/Library/Frameworks/Mono.framework/External/monodoc"))
103                                 enumerable = enumerable.Concat (Directory.EnumerateFiles ("/Library/Frameworks/Mono.framework/External/monodoc", "*.source"));
104
105                         var windowsPath = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "monodoc");
106                         if (Directory.Exists (windowsPath))
107                                 enumerable = enumerable.Concat (Directory.EnumerateFiles (windowsPath, "*.source"));
108
109                         return enumerable;
110                 }
111
112                 public static RootTree LoadTree (string indexDir, XmlDocument docTree, IEnumerable<string> sourceFiles)
113                 {
114                         if (docTree == null) {
115                                 docTree = new XmlDocument ();
116                                 using  (Stream manifestResourceStream = typeof (RootTree).Assembly.GetManifestResourceStream ("monodoc.xml")) {
117                                         docTree.Load (manifestResourceStream);
118                                 }
119                         }
120
121                         sourceFiles =  (sourceFiles ?? new string[0]);
122                         RootTree rootTree = new RootTree ();
123                         rootTree.basedir = indexDir;
124                         XmlNodeList xml_node_list = docTree.SelectNodes ("/node/node");
125                         rootTree.nameToNode["root"] = rootTree.RootNode;
126                         rootTree.nameToNode["libraries"] = rootTree.RootNode;
127                         rootTree.Populate (rootTree.RootNode, xml_node_list);
128
129                         if (rootTree.LookupEntryPoint ("various") == null) {
130                                 Console.Error.WriteLine ("No 'various' doc node! Check monodoc.xml!");
131                                 Node rootNode = rootTree.RootNode;
132                         }
133
134                         foreach (string current in sourceFiles)
135                                 rootTree.AddSourceFile (current);
136
137                         foreach (string path in uncompiledHelpSourcePaths) {
138                                 var hs = new Providers.EcmaUncompiledHelpSource (path);
139                                 hs.RootTree = rootTree;
140                                 rootTree.helpSources.Add (hs);
141                                 string epath = "extra-help-source-" + hs.Name;
142                                 Node hsn = rootTree.RootNode.CreateNode (hs.Name, "root:/" + epath);
143                                 rootTree.nameToHelpSource [epath] = hs;
144                                 hsn.EnsureNodes ();
145                                 foreach (Node n in hs.Tree.RootNode.ChildNodes)
146                                         hsn.AddNode (n);
147                         }
148
149                         RootTree.PurgeNode (rootTree.RootNode);
150                         rootTree.RootNode.Sort ();
151                         return rootTree;
152                 }
153
154                 public void AddSource (string sourcesDir)
155                 {
156                         IEnumerable<string> enumerable = Directory.EnumerateFiles (sourcesDir, "*.source");
157                         foreach (string current in enumerable)
158                                 if (!this.AddSourceFile (current))
159                                         Console.Error.WriteLine ("Error: Could not load source file {0}", current);
160                 }
161
162                 public bool AddSourceFile (string sourceFile)
163                 {
164                         if (this.loadedSourceFiles.Contains (sourceFile))
165                                 return false;
166
167                         Node node = this.LookupEntryPoint ("various") ?? base.RootNode;
168                         XmlDocument xmlDocument = new XmlDocument ();
169                         try {
170                                 xmlDocument.Load (sourceFile);
171                         } catch {
172                                 bool result = false;
173                                 return result;
174                         }
175
176                         XmlNodeList extra_nodes = xmlDocument.SelectNodes ("/monodoc/node");
177                         if (extra_nodes.Count > 0)
178                                 this.Populate (node, extra_nodes);
179
180                         XmlNodeList sources = xmlDocument.SelectNodes ("/monodoc/source");
181                         if (sources == null) {
182                                 Console.Error.WriteLine ("Error: No <source> section found in the {0} file", sourceFile);
183                                 return false;
184                         }
185
186                         loadedSourceFiles.Add (sourceFile);
187                         foreach (XmlNode xmlNode in sources) {
188                                 XmlAttribute a = xmlNode.Attributes["provider"];
189                                 if (a == null) {
190                                         Console.Error.WriteLine ("Error: no provider in <source>");
191                                         continue;
192                                 }
193                                 string provider = a.InnerText;
194                                 a = xmlNode.Attributes["basefile"];
195                                 if (a == null) {
196                                         Console.Error.WriteLine ("Error: no basefile in <source>");
197                                         continue;
198                                 }
199                                 string basefile = a.InnerText;
200                                 a = xmlNode.Attributes["path"];
201                                 if (a == null) {
202                                         Console.Error.WriteLine ("Error: no path in <source>");
203                                         continue;
204                                 }
205                                 string path = a.InnerText;
206                                 string basefilepath = Path.Combine (Path.GetDirectoryName (sourceFile), basefile);
207                                 HelpSource helpSource = RootTree.GetHelpSource (provider, basefilepath);
208                                 if (helpSource != null) {
209                                         helpSource.RootTree = this;
210                                         this.helpSources.Add (helpSource);
211                                         this.nameToHelpSource[path] = helpSource;
212                                         Node node2 = this.LookupEntryPoint (path);
213                                         if (node2 == null) {
214                                                 Console.Error.WriteLine ("node `{0}' is not defined on the documentation map", path);
215                                                 node2 = node;
216                                         }
217                                         foreach (Node current in helpSource.Tree.RootNode.ChildNodes) {
218                                                 node2.AddNode (current);
219                                         }
220                                         node2.Sort ();
221                                 }
222                         }
223                         return true;
224                 }
225
226                 static bool PurgeNode (Node node)
227                 {
228                         bool result = false;
229                         if (!node.Documented)
230                         {
231                                 List<Node> list = new List<Node> ();
232                                 foreach (Node current in node.ChildNodes)
233                                 {
234                                         bool flag = RootTree.PurgeNode (current);
235                                         if (flag)
236                                         {
237                                                 list.Add (current);
238                                         }
239                                 }
240                                 result =  (node.ChildNodes.Count == list.Count);
241                                 foreach (Node current2 in list)
242                                 {
243                                         node.DeleteNode (current2);
244                                 }
245                         }
246                         return result;
247                 }
248
249                 public static string[] GetSupportedFormats ()
250                 {
251                         return new string[]
252                         {
253                                 "ecma",
254                                 "ecmaspec",
255                                 "error",
256                                 "man",
257                                 "xhtml"
258                         };
259                 }
260
261                 public static HelpSource GetHelpSource (string provider, string basefilepath)
262                 {
263                         HelpSource result;
264                         try {
265                                 switch (provider) {
266                                 case "xhtml":
267                                 case "hb":
268                                         result = new XhtmlHelpSource (basefilepath, false);
269                                         break;
270                                 case "man":
271                                         result = new ManHelpSource (basefilepath, false);
272                                         break;
273                                 case "error":
274                                         result = new ErrorHelpSource (basefilepath, false);
275                                         break;
276                                 case "ecmaspec":
277                                         result = new EcmaSpecHelpSource (basefilepath, false);
278                                         break;
279                                 case "ecma":
280                                         result = new EcmaHelpSource (basefilepath, false);
281                                         break;
282                                 default:
283                                         Console.Error.WriteLine ("Error: Unknown provider specified: {0}", provider);
284                                         result = null;
285                                         break;
286                                 }
287                         } catch (FileNotFoundException) {
288                                 Console.Error.WriteLine ("Error: did not find one of the files in sources/" + basefilepath);
289                                 result = null;
290                         }
291                         return result;
292                 }
293
294                 public static Provider GetProvider (string provider, params string[] basefilepaths)
295                 {
296                         switch (provider) {
297                         case "ecma":
298                                 return new EcmaProvider (basefilepaths[0]);
299                         case "ecmaspec":
300                                 return new EcmaSpecProvider (basefilepaths[0]);
301                         case "error":
302                                 return new ErrorProvider (basefilepaths[0]);
303                         case "man":
304                                 return new ManProvider (basefilepaths);
305                         case "xhml":
306                         case "hb":
307                                 return new XhtmlProvider (basefilepaths[0]);
308                         }
309
310                         throw new NotSupportedException (provider);
311                 }
312
313                 void Populate (Node parent, XmlNodeList xml_node_list)
314                 {
315                         foreach (XmlNode xmlNode in xml_node_list) {
316                                 XmlAttribute e = xmlNode.Attributes["parent"];
317                                 Node parent2 = null;
318                                 if (e != null && this.nameToNode.TryGetValue (e.InnerText, out parent2)) {
319                                         xmlNode.Attributes.Remove (e);
320                                         Populate (parent2, xmlNode.SelectNodes ("."));
321                                         continue;
322                                 }
323                                 e = xmlNode.Attributes["label"];
324                                 if (e == null) {
325                                         Console.Error.WriteLine ("`label' attribute missing in <node>");
326                                         continue;
327                                 }
328                                 string label = e.InnerText;
329                                 e = xmlNode.Attributes["name"];
330                                 if (e == null) {
331                                         Console.Error.WriteLine ("`name' attribute missing in <node>");
332                                         continue;
333                                 }
334                                 string name = e.InnerText;
335                                 Node orCreateNode = parent.GetOrCreateNode (label, "root:/" + name);
336                                 orCreateNode.EnsureNodes ();
337                                 this.nameToNode[name] = orCreateNode;
338                                 XmlNodeList xmlNodeList = xmlNode.SelectNodes ("./node");
339                                 if (xmlNodeList != null) {
340                                         this.Populate (orCreateNode, xmlNodeList);
341                                 }
342                         }
343                 }
344
345                 public Node LookupEntryPoint (string name)
346                 {
347                         Node result = null;
348                         if (!this.nameToNode.TryGetValue (name, out result)) {
349                                 result = null;
350                         }
351                         return result;
352                 }
353
354                 public TOutput RenderUrl<TOutput> (string url, IDocGenerator<TOutput> generator, HelpSource hintSource = null)
355                 {
356                         Node dummy;
357                         return RenderUrl<TOutput> (url, generator, out dummy, hintSource);
358                 }
359
360                 public TOutput RenderUrl<TOutput> (string url, IDocGenerator<TOutput> generator, out Node node, HelpSource hintSource = null)
361                 {
362                         node = null;
363                         string internalId = null;
364                         Dictionary<string, string> context = null;
365                         HelpSource hs = GetHelpSourceAndIdForUrl (url, hintSource, out internalId, out context, out node);
366                         return generator.Generate (hs, internalId, context);
367                 }
368
369                 public HelpSource GetHelpSourceAndIdForUrl (string url, out string internalId, out Dictionary<string, string> context)
370                 {
371                         Node dummy;
372                         return GetHelpSourceAndIdForUrl (url, out internalId, out context, out dummy);
373                 }
374
375                 public HelpSource GetHelpSourceAndIdForUrl (string url, out string internalId, out Dictionary<string, string> context, out Node node)
376                 {
377                         return GetHelpSourceAndIdForUrl (url, null, out internalId, out context, out node);
378                 }
379
380                 public HelpSource GetHelpSourceAndIdForUrl (string url, HelpSource hintSource, out string internalId, out Dictionary<string, string> context, out Node node)
381                 {
382                         node = null;
383                         internalId = null;
384                         context = null;
385
386                         if (url.StartsWith ("root:/", StringComparison.OrdinalIgnoreCase))
387                                 return this.GetHelpSourceAndIdFromName (url.Substring ("root:/".Length), out internalId, out node);
388
389                         HelpSource helpSource = hintSource;
390                         if (helpSource == null || string.IsNullOrEmpty (internalId = helpSource.GetInternalIdForUrl (url, out node, out context))) {
391                                 helpSource = null;
392                                 foreach (var hs in helpSources.Where (h => h.CanHandleUrl (url))) {
393                                         if (!string.IsNullOrEmpty (internalId = hs.GetInternalIdForUrl (url, out node, out context))) {
394                                                 helpSource = hs;
395                                                 break;
396                                         }
397                                 }
398                         }
399
400                         return helpSource;
401                 }
402
403                 public HelpSource GetHelpSourceAndIdFromName (string name, out string internalId, out Node node)
404                 {
405                         internalId = "root:";
406                         node = this.LookupEntryPoint (name);
407
408                         return node == null ? null : node.ChildNodes.Select (n => n.Tree.HelpSource).Where (hs => hs != null).Distinct ().FirstOrDefault ();
409                 }
410
411                 public HelpSource GetHelpSourceFromId (int id)
412                 {
413                         return  (id < 0 || id >= this.helpSources.Count) ? null : this.helpSources[id];
414                 }
415
416                 public Stream GetImage (string url)
417                 {
418                         if (url.StartsWith ("source-id:", StringComparison.OrdinalIgnoreCase)) {
419                                 string text = url.Substring (10);
420                                 int num = text.IndexOf (":");
421                                 string text2 = text.Substring (0, num);
422                                 int id = 0;
423                                 if (!int.TryParse (text2, out id)) {
424                                         Console.Error.WriteLine ("Failed to parse source-id url: {0} `{1}'", url, text2);
425                                         return null;
426                                 }
427                                 HelpSource helpSourceFromId = this.GetHelpSourceFromId (id);
428                                 return helpSourceFromId.GetImage (text.Substring (num + 1));
429                         }
430                         Assembly assembly = Assembly.GetAssembly (typeof (RootTree));
431                         return assembly.GetManifestResourceStream (url);
432                 }
433
434                 public IndexReader GetIndex ()
435                 {
436                         var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "monodoc.index"));
437                         var p = paths.FirstOrDefault (File.Exists);
438                         return p == null ? (IndexReader)null : IndexReader.Load (p);
439                 }
440
441                 public static void MakeIndex ()
442                 {
443                         RootTree rootTree = RootTree.LoadTree ();
444                         rootTree.GenerateIndex ();
445                 }
446
447                 public bool GenerateIndex ()
448                 {
449                         IndexMaker indexMaker = new IndexMaker ();
450                         foreach (HelpSource current in this.helpSources)
451                                 current.PopulateIndex (indexMaker);
452
453                         var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "monodoc.index"));
454                         bool successful = false;
455
456                         foreach (var path in paths) {
457                                 try {
458                                         indexMaker.Save (path);
459                                         successful = true;
460                                         if (RootTree.IsUnix)
461                                                 RootTree.chmod (path, 420);
462                                 } catch (UnauthorizedAccessException) {
463                                 }
464                         }
465                         if (!successful) {
466                                 Console.WriteLine ("You don't have permissions to write on any of [" + string.Join (", ", paths) + "]");
467                                 return false;
468                         }
469
470                         Console.WriteLine ("Documentation index updated");
471                         return true;
472                 }
473
474                 public SearchableIndex GetSearchIndex ()
475                 {
476                         var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "search_index"));
477                         var p = paths.FirstOrDefault (Directory.Exists);
478                         return p == null ? (SearchableIndex)null : SearchableIndex.Load (p);
479                 }
480
481                 public static void MakeSearchIndex ()
482                 {
483                         RootTree rootTree = RootTree.LoadTree ();
484                         rootTree.GenerateSearchIndex ();
485                 }
486
487                 public bool GenerateSearchIndex ()
488                 {
489                         Console.WriteLine ("Loading the monodoc tree...");
490                         IndexWriter indexWriter = null;
491                         var analyzer = new StandardAnalyzer (Lucene.Net.Util.Version.LUCENE_CURRENT);
492                         var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "search_index"));
493                         bool successful = false;
494
495                         foreach (var path in paths) {
496                                 try {
497                                         if (!Directory.Exists (path))
498                                                 Directory.CreateDirectory (path);
499                                         var directory = Lucene.Net.Store.FSDirectory.Open (path);
500                                         indexWriter = new IndexWriter (directory, analyzer, true, IndexWriter.MaxFieldLength.LIMITED);
501                                         successful = true;
502                                 } catch (UnauthorizedAccessException) {}
503                         }
504                         if (!successful) {
505                                 Console.WriteLine ("You don't have permissions to write on any of [" + string.Join (", ", paths) + "]");
506                                 return false;
507                         }
508                         Console.WriteLine ("Collecting and adding documents...");
509                         foreach (HelpSource current in this.helpSources) {
510                                 current.PopulateSearchableIndex (indexWriter);
511                         }
512                         Console.WriteLine ("Closing...");
513                         indexWriter.Optimize ();
514                         indexWriter.Close ();
515                         return true;
516                 }
517
518                 [DllImport ("libc")]
519                 static extern int chmod (string filename, int mode);
520
521                 IEnumerable<string> GetIndexesPathPrefixes ()
522                 {
523                         yield return basedir;
524                         yield return Config.Get ("docPath");
525                         var indexDirectory = Config.Get ("monodocIndexDirectory");
526                         if (!string.IsNullOrEmpty (indexDirectory))
527                                 yield return indexDirectory;
528                         yield return Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData), "monodoc");
529                 }
530         }
531 }