Configuration system for beagle (XmlSerializer-based)

Designed to give the users more control.

Currently this just gives access to add more roots.
Possible ideas for future configuration include enabling/disabling backends,
setting default query domain to local/global, setting default number of hits
per backend, see the wiki for other ideas.

This is quite early on, just a preview to hopefully get some comments on the
design. I keep changing my mind so any comments are appreciated :)

Here's how it works:

- Beagle.Util.Conf is a new class which provides access to read and modify
  the current configuration.

- Beagle.Util.Conf is split up into "sections". There is currently only one
  section: indexing.

- Each section has a series of "options" which can be invoked by the user or by
  other areas of beagle code. For example, BU.Conf.Indexing has an "AddRoot"
  section option.

- On startup, the config is loaded. Each area of code which will listen for
  config options has to use BU.Conf to get an initial configuration.

- There is a command-line interface to Beagle.Util.Conf, called beagle-config.
  beagle-config allows execution of "section options" in Beagle.Util.Conf

- Config settings are stored as serialized XML objects, in ~/.beagle/config/
  There is one file per config section.

- There's quite a bit left to do, see the changes made to the TODO file.

diff -urNpX dontdiff beagle/beagled/BeagleDaemon.cs beagle-xmlser/beagled/BeagleDaemon.cs
--- beagle/beagled/BeagleDaemon.cs	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/beagled/BeagleDaemon.cs	2005-05-21 22:49:26.000000000 +0100
@@ -233,6 +233,8 @@ namespace Beagle.Daemon {
 
 			SetupSignalHandlers ();
 
+			Conf.Load ();
+
 			// Fire up our server
 			if (! StartServer ()) {
 				if (arg_replace)
diff -urNpX dontdiff beagle/beagled/FileSystemQueryable/FileNameFilter.cs beagle-xmlser/beagled/FileSystemQueryable/FileNameFilter.cs
--- beagle/beagled/FileSystemQueryable/FileNameFilter.cs	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/beagled/FileSystemQueryable/FileNameFilter.cs	2005-05-21 22:49:27.000000000 +0100
@@ -36,12 +36,18 @@ namespace Beagle.Daemon.FileSystemQuerya
 	public class FileNameFilter {
 		
 		static string home_dir;
+		private FileSystemModel model = null;
 
 		static FileNameFilter ()
 		{
 			home_dir = PathFinder.HomeDir;
 		}
 
+		public FileNameFilter (FileSystemModel model) : this ()
+		{
+			this.model = model;
+		}
+
 
 		private class Pattern {
 			private string exactMatch;
@@ -278,9 +284,10 @@ namespace Beagle.Daemon.FileSystemQuerya
 		{
 			if (! Path.IsPathRooted (path))
 				path = Path.GetFullPath (path);
-			
-			if (path == home_dir)
-				return false;
+
+			foreach (FileSystemModel.Directory root in model.Roots)
+				if (root.FullName == path)
+					return false;
 
 			string name = Path.GetFileName (path);
 
diff -urNpX dontdiff beagle/beagled/FileSystemQueryable/FileSystemModel.cs beagle-xmlser/beagled/FileSystemQueryable/FileSystemModel.cs
--- beagle/beagled/FileSystemQueryable/FileSystemModel.cs	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/beagled/FileSystemQueryable/FileSystemModel.cs	2005-05-21 22:49:27.000000000 +0100
@@ -413,7 +413,7 @@ namespace Beagle.Daemon.FileSystemQuerya
 		Hashtable path_cache = new Hashtable ();
 		Hashtable by_unique_id = new Hashtable ();
 		Queue to_be_scanned = new Queue ();
-		FileNameFilter filter = new FileNameFilter ();
+		FileNameFilter filter;
 
 		IFileEventBackend event_backend;
 		int needs_crawl_count = 0;
@@ -432,6 +432,7 @@ namespace Beagle.Daemon.FileSystemQuerya
 
 			unique_id_store = new UniqueIdStore (index_directory, index_fingerprint);
 			name_index = new NameIndex (index_directory, index_fingerprint);
+			filter = new FileNameFilter (this);
 
 			IFileAttributesStore backing_store_i;
 			backing_store_i = new FileAttributesStore_Mixed (index_directory, index_fingerprint);
diff -urNpX dontdiff beagle/beagled/FileSystemQueryable/FileSystemQueryable.cs beagle-xmlser/beagled/FileSystemQueryable/FileSystemQueryable.cs
--- beagle/beagled/FileSystemQueryable/FileSystemQueryable.cs	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/beagled/FileSystemQueryable/FileSystemQueryable.cs	2005-05-21 22:49:27.000000000 +0100
@@ -272,8 +272,11 @@ namespace Beagle.Daemon.FileSystemQuerya
 		{
 			event_backend.Start (this);
 
-			// FIXME: Shouldn't be hard-wired
-			model.AddRoot (PathFinder.HomeDir);
+			if (Conf.Indexing.IndexHomeDir)
+				model.AddRoot (PathFinder.HomeDir);
+
+			foreach (string root in Conf.Indexing.Roots)
+				model.AddRoot (root);
 
 			log.Info ("FileSystemQueryable start-up thread finished");
 			
diff -urNpX dontdiff beagle/doc/TODO beagle-xmlser/doc/TODO
--- beagle/doc/TODO	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/doc/TODO	2005-05-21 22:49:22.000000000 +0100
@@ -55,3 +55,15 @@ Mail
   text/plain parts of a mail; at the very least it could index the
   HTML parts of a mail message.
 
+Configuration
+-------------
+
+Updates via inotify
+Updates via FSW
+Updates via joebus
+Notification of updates to subscribed subsystems
+beagle-config manpage
+DelRoot capabilities
+Harden FSQ against non-EA/non-existant/already-added/child/parent roots
+Ignore patterns to replace .noindex/.neverindex functionality
+beagle-config GUI (fredrik?)
diff -urNpX dontdiff beagle/tools/Config.cs beagle-xmlser/tools/Config.cs
--- beagle/tools/Config.cs	1970-01-01 01:00:00.000000000 +0100
+++ beagle-xmlser/tools/Config.cs	2005-05-21 22:49:23.000000000 +0100
@@ -0,0 +1,170 @@
+//
+// Config.cs
+//
+// Copyright (C) 2005 Novell, Inc.
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Reflection;
+
+using Beagle;
+using Beagle.Util;
+
+using Gtk;
+
+public static class ConfigTool {
+
+	private static void PrintUsageAndExit ()
+	{
+		string usage =
+			"beagle-config: Command-line interface to the Beagle config file.\n" +
+			"Web page: http://www.gnome.org/projects/beagle\n" +
+			"Copyright (C) 2005 Novell, Inc.\n\n";
+		usage +=
+			"Usage: beagle-config [OPTIONS]\n" +
+			"   or: beagle-config <SECTION>\n" +
+			"   or: beagle-config <SECTION> <SECTIONOPTION> [PARAMS]\n\n" +
+			"Options:\n" +
+			"  --beagled-reload-config\tAsk the beagle daemon to reload\n" +
+			"                         \tthe configuration file.\n" +
+			"  --list-sections\t\tList all available configuration sections.\n" +
+			"  --help\t\t\tPrint this usage message.\n\n" +
+			"If a section is specified with no options, then a list of the available commands for that section is shown.\n";
+
+		Console.WriteLine (usage);
+
+		System.Environment.Exit (0);
+	}
+
+	private static void ListSectionsAndExit ()
+	{
+		Console.WriteLine ("Available configuration sections: ");
+		foreach (string key in Conf.Sections.Keys)
+			Console.WriteLine (" - {0}", key);
+
+		System.Environment.Exit (0);
+	}
+	
+	private static void ListSectionOptionsAndExit (string sectionname, Hashtable options)
+	{
+		Console.WriteLine ("Available options for section '{0}':", sectionname);
+		foreach (string key in options.Keys) {
+			Console.Write (" - {0}", key);
+			if (options [key] != null)
+				Console.Write (" ({0})", options [key]);
+
+			Console.WriteLine ();
+		}
+		
+		System.Environment.Exit (0);
+	}
+		
+	public static void Main (string [] args)
+	{
+		Application.InitCheck ("beagle-config", ref args);
+
+		if (args.Length == 0)
+			PrintUsageAndExit ();
+
+		int i = 0;
+		while (i < args.Length) {
+			switch (args [i]) {
+			case "--list-sections":
+				Conf.Load ();
+				ListSectionsAndExit ();
+				return;
+
+			case "--help":
+			case "--usage":
+				PrintUsageAndExit ();
+				return;
+
+			default:
+				break;
+			}
+			++i;
+		}
+
+		Conf.Load ();
+
+		string sectionname = args [0];
+
+		if (! Conf.Sections.ContainsKey (sectionname)) {
+			Console.Error.WriteLine ("ERROR: Invalid section name '{0}'", sectionname);
+			Environment.Exit (-1);
+		}
+
+		ConfigFile section = (ConfigFile) Conf.Sections [sectionname];
+		Hashtable options = Conf.GetOptions (section);
+
+		// No option specified?
+		if (args.Length == 1)
+			ListSectionOptionsAndExit (sectionname, options);
+		
+		string optionname = args [1];
+		if (! options.ContainsKey (optionname)) {
+			Console.Error.WriteLine ("ERROR: Invalid option name '{0}'", optionname);
+			Environment.Exit (-2);
+		}
+
+		//
+		// Invoke the method the user has chosen
+		//
+
+		// Pack the remaining command line params into an array used for
+		// params of the method.
+		string [] optionparams = new string [args.Length - 2];
+		int j, k;
+		for (j = 0, k = 2; k < args.Length; j++, k++)
+			optionparams [j] = args [k];
+
+		// Invoke the method
+		string output = null;
+		bool result = false;
+
+		try {
+			result = Conf.InvokeOption (section, optionname, optionparams, out output);
+		} catch (Exception e) {
+			Console.Error.WriteLine("ERROR: Command execution failed - caught exception.");
+			Console.Error.WriteLine(e.Message);
+			Environment.Exit (-3);
+		}
+
+		// Read the result and show the output
+		if (result == true)
+			Console.WriteLine (output);
+		else {
+			Console.Error.WriteLine ("ERROR: Command execution failed.");
+			Console.Error.WriteLine (output);
+			Environment.Exit (-4);
+		}
+
+		Conf.Save ();
+		Environment.Exit (0);
+
+	}
+
+
+}
diff -urNpX dontdiff beagle/tools/Makefile.am beagle-xmlser/tools/Makefile.am
--- beagle/tools/Makefile.am	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/tools/Makefile.am	2005-05-21 22:49:23.000000000 +0100
@@ -64,17 +64,30 @@ $(EXERCISE_FILE_SYSTEM_WRAPPER): $(WRAPP
 	$(WRAPPER_SED) -e "s|\@target\@|$(EXERCISE_FILE_SYSTEM_TARGET)|g" < $(srcdir)/$(WRAPPER_IN) > $@
 	chmod +x $(EXERCISE_FILE_SYSTEM_WRAPPER)
 
+CONFIG_TARGET = Config.exe
+CONFIG_WRAPPER = beagle-config
+CONFIG_CSFILES = $(srcdir)/Config.cs
+
+$(CONFIG_TARGET): $(CONFIG_CSFILES) $(LOCAL_ASSEMBLIES)
+	$(CSC) -out:$@ $(CSFLAGS) $(CONFIG_CSFILES) $(ASSEMBLIES)
+
+$(CONFIG_WRAPPER): $(WRAPPER_IN)
+	$(WRAPPER_SED) -e "s|\@target\@|$(CONFIG_TARGET)|g" < $(srcdir)/$(WRAPPER_IN) > $@
+	chmod +x $(CONFIG_WRAPPER)
+
 TARGETS = 				\
 	$(INFO_TARGET)			\
 	$(SHUTDOWN_TARGET)		\
 	$(QUERY_TARGET)			\
-	$(EXERCISE_FILE_SYSTEM_TARGET)
+	$(EXERCISE_FILE_SYSTEM_TARGET)	\
+	$(CONFIG_TARGET)
 
 WRAPPERS = 				\
 	$(INFO_WRAPPER)			\
 	$(SHUTDOWN_WRAPPER)		\
 	$(QUERY_WRAPPER)		\
-	$(EXERCISE_FILE_SYSTEM_WRAPPER)
+	$(EXERCISE_FILE_SYSTEM_WRAPPER)	\
+	$(CONFIG_WRAPPER)
 
 bin_SCRIPTS = 			\
 	$(WRAPPERS) 		\
diff -urNpX dontdiff beagle/Util/Conf.cs beagle-xmlser/Util/Conf.cs
--- beagle/Util/Conf.cs	1970-01-01 01:00:00.000000000 +0100
+++ beagle-xmlser/Util/Conf.cs	2005-05-21 22:49:22.000000000 +0100
@@ -0,0 +1,208 @@
+//
+// Conf.cs
+//
+// Copyright (C) 2005 Novell, Inc.
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.Collections;
+using System.IO;
+using System.Reflection;
+using System.Xml.Serialization;
+
+using Beagle.Util;
+namespace Beagle.Util {
+
+	public static class Conf {
+
+		public static Hashtable Sections;
+		public static IndexingConfig Indexing = null;
+		private static string configs_dir;
+
+		static Conf ()
+		{
+			Sections = new Hashtable (1);
+			configs_dir = Path.Combine (PathFinder.StorageDir, "config");
+			if (!Directory.Exists (configs_dir))
+				Directory.CreateDirectory (configs_dir);
+		}
+
+		public static void Load ()
+		{
+			//FIXME should be more organised
+			Logger.Log.Debug ("Loading config files");
+			ConfigFile temp = (ConfigFile) Indexing;
+			if (! LoadFile ("indexing.xml", typeof (IndexingConfig), out temp))
+				temp = new IndexingConfig ();
+			Indexing = (IndexingConfig) temp;
+
+			Sections.Add ("indexing", Indexing);
+		}
+
+		public static void Save ()
+		{
+			Save (false);
+		}
+
+		public static void Save (bool force)
+		{
+			foreach (DictionaryEntry entry in Sections) {
+				string filename = (string) entry.Key + ".xml";
+				ConfigFile config_file = (ConfigFile) entry.Value;
+				if (force || config_file.SaveNeeded)
+					SaveFile (filename, config_file);
+			}
+		}
+
+		// FIXME : filename should be extracted from the target type metadata
+		private static bool LoadFile (string filename, Type type, out ConfigFile config_file)
+		{
+			try {
+				FileStream fs = File.OpenRead (Path.Combine (configs_dir, filename));
+				XmlSerializer serializer = new XmlSerializer (type);
+				config_file = (ConfigFile) serializer.Deserialize (fs);
+				fs.Close ();
+				return true;
+			} catch (Exception) {
+				config_file = null;
+				return false;
+			}
+		}
+
+		// FIXME : filename should be extracted from the target type metadata
+		private static bool SaveFile (string filename, ConfigFile config_file)
+		{
+			Type type = config_file.GetType ();
+			FileStream fs = new FileStream (Path.Combine (configs_dir, filename), FileMode.Create);
+			XmlSerializer serializer = new XmlSerializer (type);
+			serializer.Serialize (fs, config_file);
+			fs.Close ();
+			return false;
+		}
+
+		// Lists all config file options in a hash table where key is option name,
+		// and value is description.
+		public static Hashtable GetOptions (ConfigFile config_file)
+		{
+			Hashtable options = new Hashtable ();
+			MemberInfo [] members = config_file.GetType ().GetMembers ();
+
+			// Find all of the methods ("options") inside the specified section
+			// object which have the ConfigOption attribute.
+			foreach (MemberInfo member in members) {
+				object [] attrs = member.GetCustomAttributes (typeof (ConfigOption), false);
+				if (attrs.Length == 0)
+					continue;
+
+				options.Add (member.Name, ((ConfigOption) attrs [0]).Description);
+			}
+
+			return options;
+		}
+
+		public static bool InvokeOption (ConfigFile section, string option, string [] args, out string output)
+		{
+			MethodInfo method = section.GetType ().GetMethod (option);
+			if (method == null)
+				throw new Exception("No such method '" + option + "' for section '" + section + "'");
+
+			object [] attrs = method.GetCustomAttributes (typeof (ConfigOption), false);
+			if (attrs.Length == 0)
+				throw new Exception ("Method '" + option + "' is not a configurable option");
+
+			// Check the required number of parameters have been provided
+			ConfigOption attr = (ConfigOption) attrs [0];
+			if (attr.Params > 0 && args.Length < attr.Params)
+				throw new Exception ("Option '" + option + "' requires " + attr.Params + " parameter(s): " + attr.ParamsDescription);
+
+			// Can't pass a ref/out in a MethodInfo.Invoke() call so we box the
+			// output in a single-element array.
+			string [] boxoutput = new string[1];
+
+			object [] methodparams = { boxoutput, args };
+			bool result = (bool) method.Invoke (section, methodparams);
+			output = boxoutput[0];
+
+			// Mark the section as save-needed if we just changed something
+			if (result && attr.IsMutator)
+				section.SaveNeeded = true;
+				
+			return result;
+		}
+
+		public class IndexingConfig : ConfigFile {
+
+			public IndexingConfig ()
+			{
+				roots = new ArrayList ();
+				index_home_dir = true;
+			}
+
+			private ArrayList roots;
+			public ArrayList Roots {
+				get { return ArrayList.ReadOnly (roots); }
+				set { roots = value; }
+			}
+
+			private bool index_home_dir;
+			public bool IndexHomeDir {
+				get { return index_home_dir; }
+				set { index_home_dir = value; }
+			}
+
+			[ConfigOption (Description="List the indexing roots")]
+			public bool ListRoots (string [] output, string [] args)
+			{
+				output [0] = "Current roots:\n";
+				if (this.index_home_dir == true)
+					output [0] += " - Your home directory\n";
+				foreach (string root in roots)
+					output [0] += " - " + root + "\n";
+
+				return true;
+			}
+
+			[ConfigOption (Description="Add a root path to be indexed", Params=1, ParamsDescription="A path", IsMutator=true)]
+			public bool AddRoot (string [] output, string [] args)
+			{
+				roots.Add (args [0]);
+				output [0] = "Root added.";
+				return true;
+			}
+		}
+
+		private class ConfigOption : Attribute {
+			public string Description;
+			public int Params;
+			public string ParamsDescription;
+			public bool IsMutator = false;
+		}
+
+	}
+
+	public class ConfigFile {
+		[XmlIgnore]
+		public bool SaveNeeded = false;
+	}
+
+}
diff -urNpX dontdiff beagle/Util/Makefile.am beagle-xmlser/Util/Makefile.am
--- beagle/Util/Makefile.am	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/Util/Makefile.am	2005-05-21 22:49:22.000000000 +0100
@@ -30,6 +30,7 @@ $(EXTSTR): $(EXTSTR_IN)
 CSFILES = 			        	\
 	$(srcdir)/camel.cs              	\
 	$(srcdir)/CommandLineFu.cs		\
+	$(srcdir)/Conf.cs			\
 	$(srcdir)/DirectoryWalker.cs		\
 	$(srcdir)/ExceptionHandlingThread.cs	\
 	$(srcdir)/ExifData.cs			\
