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