New e-d-s backend which indexes all local addressbooks and calendars.
[beagle.git] / beagled / EvolutionDataServerQueryable / EvolutionDataServerQueryable.cs
blobdc87a43c6b542870dc15cd14ce8f618ffa2284ee
1 //
2 // EvolutionDataServerQueryable.cs
3 //
4 // Copyright (C) 2004 Novell, Inc.
5 //
7 //
8 // Permission is hereby granted, free of charge, to any person obtaining a
9 // copy of this software and associated documentation files (the "Software"),
10 // to deal in the Software without restriction, including without limitation
11 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 // and/or sell copies of the Software, and to permit persons to whom the
13 // Software is furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in
16 // all copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 // DEALINGS IN THE SOFTWARE.
28 using System;
29 using System.Collections;
30 using System.Globalization;
31 using System.Text;
32 using System.Threading;
33 using System.IO;
35 using Beagle.Daemon;
36 using Beagle.Util;
38 using Evolution;
40 namespace Beagle.Daemon.EvolutionDataServerQueryable {
42 [QueryableFlavor (Name="EvolutionDataServer", Domain=QueryDomain.Local, RequireInotify=false)]
43 public class EvolutionDataServerQueryable : LuceneQueryable {
44 //private Scheduler.Priority priority = Scheduler.Priority.Immediate;
45 private Scheduler.Priority priority = Scheduler.Priority.Delayed;
47 private string photo_dir;
48 private DateTime sequence_start_time;
50 private DateTime addressbook_indexed_through = DateTime.MinValue;
51 private DateTime calendar_indexed_through = DateTime.MinValue;
53 public EvolutionDataServerQueryable () : base ("EvolutionDataServerIndex")
55 photo_dir = Path.Combine (Driver.IndexDirectory, "Photos");
56 System.IO.Directory.CreateDirectory (photo_dir);
59 public override void Start ()
61 base.Start ();
63 Logger.Log.Info ("Scanning addressbooks and calendars");
64 Stopwatch timer = new Stopwatch ();
65 timer.Start ();
67 EdsSource src;
69 src = new EdsSource ("/apps/evolution/addressbook/sources");
70 src.IndexSourceAll += AddressbookIndexSourceAll;
71 src.IndexSourceChanges += AddressbookIndexSourceChanges;
72 src.RemoveSource += AddressbookRemoveSource;
73 src.Index ();
75 src = new EdsSource ("/apps/evolution/calendar/sources");
76 src.IndexSourceAll += CalendarIndexSourceAll;
77 src.IndexSourceChanges += CalendarIndexSourceChanges;
78 src.RemoveSource += CalendarRemoveSource;
79 src.Index ();
81 timer.Stop ();
82 Logger.Log.Info ("Scanned addressbooks and calendars in {0}", timer);
85 public void Add (Indexable indexable, Scheduler.Priority priority)
87 Scheduler.Task task;
88 task = NewAddTask (indexable);
89 task.Priority = priority;
90 ThisScheduler.Add (task);
93 public void Remove (Uri uri)
95 Scheduler.Task task;
96 task = NewRemoveTask (uri);
97 task.Priority = Scheduler.Priority.Immediate;
98 ThisScheduler.Add (task);
101 ///////////////////////////////////////
103 private void AddressbookIndexSourceAll (Evolution.Source src)
105 if (!src.IsLocal ()) {
106 Logger.Log.Debug ("Skipping remote addressbook {0}", src.Uri);
107 return;
110 Logger.Log.Debug ("Indexing all data in this addressbook ({0})!", src.Uri);
112 Book book = new Book (src);
113 book.Open (true);
115 BookView book_view = book.GetBookView (BookQuery.AnyFieldContains (""),
116 new object [0],
117 -1);
119 book_view.ContactsAdded += OnContactsAdded;
120 book_view.ContactsRemoved += OnContactsRemoved;
121 book_view.ContactsChanged += OnContactsChanged;
122 book_view.SequenceComplete += OnSequenceComplete;
124 book_view.Start ();
127 private void AddressbookIndexSourceChanges (Evolution.Source src)
129 if (!src.IsLocal ()) {
130 Logger.Log.Debug ("Skipping remote addressbook {0}", src.Uri);
131 return;
134 Book book = new Book (src);
135 book.Open (true);
137 Contact[] added, changed;
138 string[] removed;
140 Logger.Log.Debug ("Getting addressbook changes for {0}", src.Uri);
141 book.GetChanges ("beagle-" + Driver.Fingerprint, out added, out changed, out removed);
142 Logger.Log.Debug ("Addressbook {0}: {1} added, {2} changed, {3} removed",
143 book.Uri, added.Length, changed.Length, removed.Length);
145 foreach (Contact contact in added)
146 AddContact (contact);
148 foreach (Contact contact in changed)
149 AddContact (contact);
151 foreach (string id in removed)
152 RemoveContact (id);
154 BookView book_view = book.GetBookView (BookQuery.AnyFieldContains (""),
155 new object [0],
156 -1);
158 book_view.ContactsAdded += OnContactsAdded;
159 book_view.ContactsRemoved += OnContactsRemoved;
160 book_view.ContactsChanged += OnContactsChanged;
161 book_view.SequenceComplete += OnSequenceComplete;
163 book_view.Start ();
166 private void AddressbookRemoveSource (Evolution.Source src)
168 // FIXME: We need to index the group's UID and then
169 // we need a way to schedule removal tasks for
170 // anything that matches that lucene property
171 Logger.Log.Debug ("FIXME: Remove addressbook source {0}", src.Uri);
174 private static Uri GetContactUri (Evolution.Contact contact) {
175 return GetContactUri (contact.Id);
178 private static Uri GetContactUri (string id) {
179 return new Uri ("contact://" + id, true); // FIXME!
182 private DateTime AddressbookIndexedThrough {
184 get {
185 if (addressbook_indexed_through == DateTime.MinValue) {
186 string filename = Path.Combine (IndexStoreDirectory, "AddressbookIndexedThrough");
188 string line = null;
189 try {
190 StreamReader sr = new StreamReader (filename);
191 line = sr.ReadLine ();
192 sr.Close ();
193 } catch (Exception ex) { }
195 if (line != null)
196 addressbook_indexed_through = StringFu.StringToDateTime (line);
198 return addressbook_indexed_through;
201 set {
202 addressbook_indexed_through = value;
204 string filename = Path.Combine (IndexStoreDirectory, "AddressbookIndexedThrough");
205 StreamWriter sw = new StreamWriter (filename);
206 sw.WriteLine (StringFu.DateTimeToString (addressbook_indexed_through));
207 sw.Close ();
211 private static DateTime RevStringToDateTime (string date_str)
213 if (date_str == null)
214 return DateTime.MinValue;
216 string[] formats = {
217 "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'",
218 "yyyyMMdd'T'HHmmss'Z'"
221 try {
222 return DateTime.ParseExact (date_str, formats,
223 CultureInfo.InvariantCulture,
224 DateTimeStyles.None);
225 } catch (FormatException) {
226 Logger.Log.Warn ("Unable to parse last revision string: {0}", date_str);
227 return DateTime.MinValue;
231 private Indexable ContactToIndexable (Evolution.Contact contact)
233 DateTime rev = RevStringToDateTime (contact.Rev);
235 if (rev != DateTime.MinValue && rev < AddressbookIndexedThrough)
236 return null;
238 Indexable indexable = new Indexable (GetContactUri (contact));
239 indexable.Timestamp = rev;
240 indexable.Type = "Contact";
242 indexable.AddProperty (Property.New ("fixme:FileAs", contact.FileAs));
243 indexable.AddProperty (Property.New ("fixme:GivenName", contact.GivenName));
244 indexable.AddProperty (Property.New ("fixme:FamilyName", contact.FamilyName));
245 indexable.AddProperty (Property.New ("fixme:Nickname", contact.Nickname));
246 indexable.AddProperty (Property.New ("fixme:AddressLabelHome", contact.AddressLabelHome));
247 indexable.AddProperty (Property.New ("fixme:AddressLabelWork", contact.AddressLabelWork));
248 indexable.AddProperty (Property.New ("fixme:AddressLabelOther", contact.AddressLabelOther));
249 indexable.AddProperty (Property.New ("fixme:AssistantPhone", contact.AssistantPhone));
250 indexable.AddProperty (Property.New ("fixme:BusinessPhone", contact.BusinessPhone));
251 indexable.AddProperty (Property.New ("fixme:BusinessPhone2", contact.BusinessPhone2));
252 indexable.AddProperty (Property.New ("fixme:BusinessFax", contact.BusinessFax));
253 indexable.AddProperty (Property.New ("fixme:CallbackPhone", contact.CallbackPhone));
254 indexable.AddProperty (Property.New ("fixme:CarPhone", contact.CarPhone));
255 indexable.AddProperty (Property.New ("fixme:CompanyPhone", contact.CompanyPhone));
256 indexable.AddProperty (Property.New ("fixme:HomePhone", contact.HomePhone));
257 indexable.AddProperty (Property.New ("fixme:HomePhone2", contact.HomePhone2));
258 indexable.AddProperty (Property.New ("fixme:HomeFax", contact.HomeFax));
259 indexable.AddProperty (Property.New ("fixme:IsdnPhone", contact.IsdnPhone));
260 indexable.AddProperty (Property.New ("fixme:MobilePhone", contact.MobilePhone));
261 indexable.AddProperty (Property.New ("fixme:OtherPhone", contact.OtherPhone));
262 indexable.AddProperty (Property.New ("fixme:OtherFax", contact.OtherFax));
263 indexable.AddProperty (Property.New ("fixme:Pager", contact.Pager));
264 indexable.AddProperty (Property.New ("fixme:PrimaryPhone", contact.PrimaryPhone));
265 indexable.AddProperty (Property.New ("fixme:Radio", contact.Radio));
266 indexable.AddProperty (Property.New ("fixme:Telex", contact.Telex));
267 indexable.AddProperty (Property.NewKeyword ("fixme:Tty", contact.Tty));
268 indexable.AddProperty (Property.NewKeyword ("fixme:Email1", contact.Email1));
269 indexable.AddProperty (Property.NewKeyword ("fixme:Email2", contact.Email2));
270 indexable.AddProperty (Property.NewKeyword ("fixme:Email3", contact.Email3));
271 indexable.AddProperty (Property.NewKeyword ("fixme:Mailer", contact.Mailer));
272 indexable.AddProperty (Property.New ("fixme:Org", contact.Org));
273 indexable.AddProperty (Property.New ("fixme:OrgUnit", contact.OrgUnit));
274 indexable.AddProperty (Property.New ("fixme:Office", contact.Office));
275 indexable.AddProperty (Property.New ("fixme:Title", contact.Title));
276 indexable.AddProperty (Property.New ("fixme:Role", contact.Role));
277 indexable.AddProperty (Property.New ("fixme:Manager", contact.Manager));
278 indexable.AddProperty (Property.New ("fixme:Assistant", contact.Assistant));
279 indexable.AddProperty (Property.NewKeyword ("fixme:HomepageUrl", contact.HomepageUrl));
280 indexable.AddProperty (Property.NewKeyword ("fixme:BlogUrl", contact.BlogUrl));
281 indexable.AddProperty (Property.NewKeyword ("fixme:Categories", contact.Categories));
282 indexable.AddProperty (Property.NewKeyword ("fixme:Caluri", contact.Caluri));
283 indexable.AddProperty (Property.NewKeyword ("fixme:Icscalendar", contact.Icscalendar));
284 indexable.AddProperty (Property.New ("fixme:Spouse", contact.Spouse));
285 indexable.AddProperty (Property.New ("fixme:Note", contact.Note));
287 Evolution.ContactPhoto photo = contact.Photo;
289 if (photo.Data != null && photo.Data.Length > 0) {
290 string photo_filename = GetPhotoFilename (contact.Id);
291 Stream s = new FileStream (photo_filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
292 BinaryWriter w = new BinaryWriter (s);
293 w.Write (photo.Data);
294 w.Close ();
295 s.Close ();
297 indexable.AddProperty (Property.NewUnsearched ("beagle:Photo", photo_filename));
299 // FIXME: List?
300 // FIXME: ListShowAddresses?
302 // FIXME: Should we not drop the extra Im addresses?
303 if (contact.ImAim.Length > 0)
304 indexable.AddProperty (Property.NewKeyword ("fixme:ImAim", contact.ImAim [0]));
305 if (contact.ImIcq.Length > 0)
306 indexable.AddProperty (Property.NewKeyword ("fixme:ImIcq", contact.ImIcq [0]));
307 if (contact.ImJabber.Length > 0)
308 indexable.AddProperty (Property.NewKeyword ("fixme:ImJabber", contact.ImJabber [0]));
309 if (contact.ImMsn.Length > 0)
310 indexable.AddProperty (Property.NewKeyword ("fixme:ImMsn", contact.ImMsn [0]));
311 if (contact.ImYahoo.Length > 0)
312 indexable.AddProperty (Property.NewKeyword ("fixme:ImYahoo", contact.ImYahoo [0]));
313 if (contact.ImGroupwise.Length > 0)
314 indexable.AddProperty (Property.NewKeyword ("fixme:ImGroupWise", contact.ImGroupwise [0]));
316 String name = "";
317 if (contact.GivenName != null && contact.GivenName != "")
318 name = contact.GivenName;
319 if (contact.FamilyName != null && contact.FamilyName != "")
320 name += " " + contact.FamilyName;
321 if (name.Length > 0)
322 indexable.AddProperty (Property.New ("fixme:Name", name));
324 if (contact.Email1 != null)
325 indexable.AddProperty (Property.NewKeyword ("fixme:Email",
326 contact.Email1));
327 return indexable;
330 private void AddContact (Contact contact)
332 Indexable indexable = ContactToIndexable (contact);
333 if (indexable != null)
334 Add (indexable, priority);
337 private void RemoveContact (string id)
339 Remove (GetContactUri (id));
341 string filename = GetPhotoFilename (id);
342 if (filename != null && File.Exists (filename))
343 File.Delete (filename);
346 private void OnContactsAdded (object o,
347 Evolution.ContactsAddedArgs args)
349 foreach (Evolution.Contact contact in args.Contacts)
350 AddContact (contact);
353 private void OnContactsChanged (object o,
354 Evolution.ContactsChangedArgs args)
356 foreach (Evolution.Contact contact in args.Contacts)
357 AddContact (contact);
360 private void OnContactsRemoved (object o,
361 Evolution.ContactsRemovedArgs args)
363 // FIXME: This is a temporary workaround for the
364 // fact that the evolution bindings return a
365 // GLib.List with an object type, but there
366 // are really strings in there
368 GLib.List id_list = new GLib.List (args.Ids.Handle,
369 typeof (string));
372 foreach (string id in id_list)
373 RemoveContact (id);
376 private string GetPhotoFilename (string id)
378 return Path.Combine (photo_dir, id);
381 private void OnSequenceComplete (object o,
382 Evolution.SequenceCompleteArgs args)
384 // Contacts that get changed while the beagled is
385 // running will be re-indexed during the next scan.
386 // That isn't optimal, but is much better than the
387 // current situation.
388 AddressbookIndexedThrough = sequence_start_time;
390 // Now that we're done synching with the original
391 // state of the addressbook, switch all new changes to
392 // Immediate mode
393 priority = Scheduler.Priority.Immediate;
396 ///////////////////////////////////////
398 private void CalendarIndexSourceAll (Evolution.Source src)
400 if (!src.IsLocal ()) {
401 Logger.Log.Debug ("Skipping remote calendar {0}", src.Uri);
402 return;
405 Logger.Log.Debug ("Indexing all data in this calendar ({0})!", src.Uri);
407 Cal cal = new Cal (src, CalSourceType.Event);
408 cal.Open (true);
410 CalComponent[] event_list = cal.GetItems ("#t");
412 Logger.Log.Debug ("Calendar has {0} items", event_list.Length);
414 foreach (CalComponent cc in event_list)
415 IndexCalComponent (cc, Scheduler.Priority.Immediate);
417 CalView cal_view = cal.GetCalView ("#t");
418 cal_view.ObjectsAdded += OnObjectsAdded;
419 cal_view.ObjectsModified += OnObjectsModified;
420 cal_view.ObjectsRemoved += OnObjectsRemoved;
421 cal_view.Start ();
424 private void CalendarIndexSourceChanges (Evolution.Source src)
426 if (!src.IsLocal ()) {
427 Logger.Log.Debug ("Skipping remote calendar {0}", src.Uri);
428 return;
431 Cal cal = new Cal (src, CalSourceType.Event);
432 cal.Open (true);
434 CalComponent[] new_items, update_items;
435 string[] remove_items;
437 Logger.Log.Debug ("Getting calendar changes for {0}", src.Uri);
438 cal.GetChanges ("beagle-" + this.Driver.Fingerprint, out new_items, out update_items, out remove_items);
439 Logger.Log.Debug ("Calendar {0}: {1} new items, {2} updated items, {3} removed items",
440 cal.Uri, new_items.Length, update_items.Length, remove_items.Length);
442 foreach (CalComponent cc in new_items)
443 IndexCalComponent (cc, Scheduler.Priority.Immediate);
445 foreach (CalComponent cc in update_items)
446 IndexCalComponent (cc, Scheduler.Priority.Immediate);
448 foreach (string id in remove_items) {
449 // FIXME: Broken in evo-sharp right now
450 //RemoveCalComponent (id);
453 CalView cal_view = cal.GetCalView ("#t");
454 cal_view.ObjectsAdded += OnObjectsAdded;
455 cal_view.ObjectsModified += OnObjectsModified;
456 cal_view.ObjectsRemoved += OnObjectsRemoved;
457 cal_view.Start ();
460 private void CalendarRemoveSource (Evolution.Source src)
462 // FIXME: We need to index the group's UID and then
463 // we need a way to schedule removal tasks for
464 // anything that matches that lucene property
465 Logger.Log.Debug ("FIXME: Remove calendar source {0}", src.Uri);
468 private static Uri GetCalendarUri (CalComponent cc) {
469 return GetContactUri (cc.Uid);
472 private static Uri GetCalendarUri (string id) {
473 return new Uri ("calendar://" + id, true); // FIXME!
476 private DateTime CalendarIndexedThrough {
478 get {
479 if (calendar_indexed_through == DateTime.MinValue) {
480 string filename = Path.Combine (IndexStoreDirectory, "CalendarIndexedThrough");
482 string line = null;
483 try {
484 StreamReader sr = new StreamReader (filename);
485 line = sr.ReadLine ();
486 sr.Close ();
487 } catch (Exception ex) { }
489 if (line != null)
490 calendar_indexed_through = StringFu.StringToDateTime (line);
492 return calendar_indexed_through;
495 set {
496 calendar_indexed_through = value;
498 string filename = Path.Combine (IndexStoreDirectory, "CalendarIndexedThrough");
499 StreamWriter sw = new StreamWriter (filename);
500 sw.WriteLine (StringFu.DateTimeToString (calendar_indexed_through));
501 sw.Close ();
505 private void IndexCalComponent (CalComponent cc, Scheduler.Priority priority)
507 Indexable indexable = CalComponentToIndexable (cc);
508 Add (indexable, priority);
511 private void RemoveCalComponent (string id)
513 Remove (GetCalendarUri (id));
516 private Indexable CalComponentToIndexable (CalComponent cc)
518 Indexable indexable = new Indexable (new Uri ("calendar:///" + cc.Uid));
520 indexable.Timestamp = cc.Dtstart;
521 indexable.Type = "Calendar";
523 indexable.AddProperty (Property.NewKeyword ("fixme:uid", cc.Uid));
524 indexable.AddProperty (Property.NewDate ("fixme:starttime", cc.Dtstart));
525 indexable.AddProperty (Property.NewDate ("fixme:endtime", cc.Dtend));
527 foreach (string attendee in cc.Attendees)
528 indexable.AddProperty (Property.New ("fixme:attendee", attendee));
530 foreach (string comment in cc.Comments)
531 indexable.AddProperty (Property.New ("fixme:comment", comment));
533 foreach (string description in cc.Descriptions)
534 indexable.AddProperty (Property.New ("fixme:description", description));
536 foreach (string summary in cc.Summaries)
537 indexable.AddProperty (Property.New ("fixme:summary", summary));
539 foreach (string category in cc.Categories)
540 indexable.AddProperty (Property.NewKeyword ("fixme:category", category));
542 foreach (string location in cc.Location)
543 indexable.AddProperty (Property.New ("fixme:location", location));
545 return indexable;
548 private void OnObjectsAdded (object o, ObjectsAddedArgs args)
550 CalView cal_view = (CalView) o;
552 foreach (CalComponent cc in CalUtil.CalCompFromICal (args.Objects.Handle, cal_view.Client)) {
553 // If the minimum date is unset/invalid,
554 // index it.
555 if (cc.LastModified <= CalUtil.MinDate || cc.LastModified > CalendarIndexedThrough)
556 IndexCalComponent (cc, Scheduler.Priority.Immediate);
560 private void OnObjectsModified (object o, ObjectsModifiedArgs args)
562 CalView cal_view = (CalView) o;
564 foreach (CalComponent cc in CalUtil.CalCompFromICal (args.Objects.Handle, cal_view.Client))
565 IndexCalComponent (cc, Scheduler.Priority.Immediate);
568 private void OnObjectsRemoved (object o, ObjectsRemovedArgs args)
570 // FIXME: This is a temporary workaround for the
571 // fact that the evolution bindings return a
572 // GLib.List with an object type, but there
573 // are really strings in there
575 GLib.List id_list = new GLib.List (args.Uids.Handle,
576 typeof (string));
578 foreach (string uid in id_list) {
579 Scheduler.Task task;
580 task = NewRemoveTask (new Uri ("calendar:///" + uid));
581 task.Priority = Scheduler.Priority.Immediate;
582 ThisScheduler.Add (task);