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