Merge pull request #600 from tr8dr/master
[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, RootNamespace + 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, RootNamespace + 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                         return result;
351                 }
352
353                 public TOutput RenderUrl<TOutput> (string url, IDocGenerator<TOutput> generator, HelpSource hintSource = null)
354                 {
355                         Node dummy;
356                         return RenderUrl<TOutput> (url, generator, out dummy, hintSource);
357                 }
358
359                 public TOutput RenderUrl<TOutput> (string url, IDocGenerator<TOutput> generator, out Node node, HelpSource hintSource = null)
360                 {
361                         node = null;
362                         string internalId = null;
363                         Dictionary<string, string> context = null;
364                         HelpSource hs = GetHelpSourceAndIdForUrl (url, hintSource, out internalId, out context, out node);
365                         return generator.Generate (hs, internalId, context);
366                 }
367
368                 public HelpSource GetHelpSourceAndIdForUrl (string url, out string internalId, out Dictionary<string, string> context)
369                 {
370                         Node dummy;
371                         return GetHelpSourceAndIdForUrl (url, out internalId, out context, out dummy);
372                 }
373
374                 public HelpSource GetHelpSourceAndIdForUrl (string url, out string internalId, out Dictionary<string, string> context, out Node node)
375                 {
376                         return GetHelpSourceAndIdForUrl (url, null, out internalId, out context, out node);
377                 }
378
379                 public HelpSource GetHelpSourceAndIdForUrl (string url, HelpSource hintSource, out string internalId, out Dictionary<string, string> context, out Node node)
380                 {
381                         node = null;
382                         internalId = null;
383                         context = null;
384
385                         if (url == "root:") {
386                                 context = new Dictionary<string, string> { {"specialpage", "master-root"} };
387                                 internalId = url;
388                                 node = null;
389                                 // We return the first help source available since the generator will simply fetch this RootTree instance through it
390                                 return helpSources.FirstOrDefault ();
391                         }
392                         if (url.StartsWith (RootNamespace, StringComparison.OrdinalIgnoreCase)) {
393                                 context = new Dictionary<string, string> { {"specialpage", "root"} };
394                                 return GetHelpSourceAndIdFromName (url.Substring (RootNamespace.Length), out internalId, out node);
395                         }
396
397                         HelpSource helpSource = hintSource;
398                         if (helpSource == null || string.IsNullOrEmpty (internalId = helpSource.GetInternalIdForUrl (url, out node, out context))) {
399                                 helpSource = null;
400                                 foreach (var hs in helpSources.Where (h => h.CanHandleUrl (url))) {
401                                         if (!string.IsNullOrEmpty (internalId = hs.GetInternalIdForUrl (url, out node, out context))) {
402                                                 helpSource = hs;
403                                                 break;
404                                         }
405                                 }
406                         }
407
408                         return helpSource;
409                 }
410
411                 public HelpSource GetHelpSourceAndIdFromName (string name, out string internalId, out Node node)
412                 {
413                         internalId = "root:";
414                         node = LookupEntryPoint (name);
415
416                         return node == null ? null : node.ChildNodes.Select (n => n.Tree.HelpSource).FirstOrDefault (hs => hs != null);
417                 }
418
419                 public HelpSource GetHelpSourceFromId (int id)
420                 {
421                         return  (id < 0 || id >= this.helpSources.Count) ? null : this.helpSources[id];
422                 }
423
424                 public Stream GetImage (string url)
425                 {
426                         if (url.StartsWith ("source-id:", StringComparison.OrdinalIgnoreCase)) {
427                                 string text = url.Substring (10);
428                                 int num = text.IndexOf (":");
429                                 string text2 = text.Substring (0, num);
430                                 int id = 0;
431                                 if (!int.TryParse (text2, out id)) {
432                                         Console.Error.WriteLine ("Failed to parse source-id url: {0} `{1}'", url, text2);
433                                         return null;
434                                 }
435                                 HelpSource helpSourceFromId = this.GetHelpSourceFromId (id);
436                                 return helpSourceFromId.GetImage (text.Substring (num + 1));
437                         }
438                         Assembly assembly = Assembly.GetAssembly (typeof (RootTree));
439                         return assembly.GetManifestResourceStream (url);
440                 }
441
442                 public IndexReader GetIndex ()
443                 {
444                         var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "monodoc.index"));
445                         var p = paths.FirstOrDefault (File.Exists);
446                         return p == null ? (IndexReader)null : IndexReader.Load (p);
447                 }
448
449                 public static void MakeIndex ()
450                 {
451                         RootTree rootTree = RootTree.LoadTree ();
452                         rootTree.GenerateIndex ();
453                 }
454
455                 public bool GenerateIndex ()
456                 {
457                         IndexMaker indexMaker = new IndexMaker ();
458                         foreach (HelpSource current in this.helpSources)
459                                 current.PopulateIndex (indexMaker);
460
461                         var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "monodoc.index"));
462                         bool successful = false;
463
464                         foreach (var path in paths) {
465                                 try {
466                                         indexMaker.Save (path);
467                                         successful = true;
468                                         if (RootTree.IsUnix)
469                                                 RootTree.chmod (path, 420);
470                                 } catch (UnauthorizedAccessException) {
471                                 }
472                         }
473                         if (!successful) {
474                                 Console.WriteLine ("You don't have permissions to write on any of [" + string.Join (", ", paths) + "]");
475                                 return false;
476                         }
477
478                         Console.WriteLine ("Documentation index updated");
479                         return true;
480                 }
481
482                 public SearchableIndex GetSearchIndex ()
483                 {
484                         var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "search_index"));
485                         var p = paths.FirstOrDefault (Directory.Exists);
486                         return p == null ? (SearchableIndex)null : SearchableIndex.Load (p);
487                 }
488
489                 public static void MakeSearchIndex ()
490                 {
491                         RootTree rootTree = RootTree.LoadTree ();
492                         rootTree.GenerateSearchIndex ();
493                 }
494
495                 public bool GenerateSearchIndex ()
496                 {
497                         Console.WriteLine ("Loading the monodoc tree...");
498                         IndexWriter indexWriter = null;
499                         var analyzer = new StandardAnalyzer (Lucene.Net.Util.Version.LUCENE_CURRENT);
500                         var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "search_index"));
501                         bool successful = false;
502
503                         foreach (var path in paths) {
504                                 try {
505                                         if (!Directory.Exists (path))
506                                                 Directory.CreateDirectory (path);
507                                         var directory = Lucene.Net.Store.FSDirectory.Open (path);
508                                         indexWriter = new IndexWriter (directory, analyzer, true, IndexWriter.MaxFieldLength.LIMITED);
509                                         successful = true;
510                                 } catch (UnauthorizedAccessException) {}
511                         }
512                         if (!successful) {
513                                 Console.WriteLine ("You don't have permissions to write on any of [" + string.Join (", ", paths) + "]");
514                                 return false;
515                         }
516                         Console.WriteLine ("Collecting and adding documents...");
517                         foreach (HelpSource current in this.helpSources) {
518                                 current.PopulateSearchableIndex (indexWriter);
519                         }
520                         Console.WriteLine ("Closing...");
521                         indexWriter.Optimize ();
522                         indexWriter.Close ();
523                         return true;
524                 }
525
526                 [DllImport ("libc")]
527                 static extern int chmod (string filename, int mode);
528
529                 IEnumerable<string> GetIndexesPathPrefixes ()
530                 {
531                         yield return basedir;
532                         yield return Config.Get ("docPath");
533                         var indexDirectory = Config.Get ("monodocIndexDirectory");
534                         if (!string.IsNullOrEmpty (indexDirectory))
535                                 yield return indexDirectory;
536                         yield return Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData), "monodoc");
537                 }
538
539                 [Obsolete]
540                 public string GetTitle (string url)
541                 {
542                         return "Mono Documentation";
543                 }
544         }
545 }