summaryrefslogtreecommitdiff
path: root/src/core/DataSource.vala
blob: 514ef9bf76a75b5d7e43d5eea8fea21f71fc4716 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
/* Copyright 2011-2015 Yorba Foundation
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later).  See the COPYING file in this distribution.
 */

//
// DataSource
// 
// A DataSource is an object that is unique throughout the system.  DataSources
// commonly have external and/or persistent representations, hence they have a notion of being
// destroyed (versus removed or freed).  Several DataViews may exist that reference a single
// DataSource.  Note that DataSources MUST be destroyed (rather than simply removed) from their
// SourceCollection, and that they MUST be destroyed via their SourceCollection (rather than
// calling DataSource.destroy() directly.)
//
// Destroying a DataSource indicates it should remove all secondary and tertiary structures (such
// as thumbnails) and any records pointing to its backing store.  SourceCollection.destroy_marked()
// has a parameter indicating if the backing should be destroyed as well; that is when
// internal_delete_backing() is called.
//
// There are no provisions (currently) for a DataSource to be removed from its SourceCollection
// without destroying its backing and/or secondary and tertiary structures.  DataSources are intended
// to go to the grave with their SourceCollection otherwise.  If a need arises for a DataSource to
// be peaceably removed from its SourceCollection, code will need to be written.  SourceSnapshots
// may be one solution to this problem.
//
// Some DataSources cannot be reconstituted (for example, if its backing file is deleted).  In
// that case, dehydrate() should return null.  When reconstituted, it is the responsibility of the
// implementation to ensure an exact clone is produced, minus any details that are not relevant or
// exposed (such as a database ID).
//
// If other DataSources refer to this DataSource, their state will *not* be 
// saved/restored.  This must be achieved via other means.  However, implementations *should*
// track when changes to external state would break the proxy and call notify_broken();
//

public abstract class DataSource : DataObject {
    protected delegate void ContactSubscriber(DataView view);
    protected delegate void ContactSubscriberAlteration(DataView view, Alteration alteration);
    
    private DataView[] subscribers = new DataView[4];
    private SourceHoldingTank holding_tank = null;
    private weak SourceCollection unlinked_from_collection = null;
    private Gee.HashMap<string, Gee.List<string>> backlinks = null;
    private bool in_contact = false;
    private bool marked_for_destroy = false;
    private bool is_destroyed = false;
    
    // This signal is fired after the DataSource has been unlinked from its SourceCollection.
    public virtual signal void unlinked(SourceCollection sources) {
    }
    
    // This signal is fired after the DataSource has been relinked to a SourceCollection.
    public virtual signal void relinked(SourceCollection sources) {
    }
    
    // This signal is fired at the end of the destroy() chain.  The object's state is either fragile
    // or unusable.  It is up to all observers to drop their references to the DataObject.
    public virtual signal void destroyed() {
    }
    
    public DataSource(int64 object_id = INVALID_OBJECT_ID) {
        base (object_id);
    }
    
    ~DataSource() {
#if TRACE_DTORS
        debug("DTOR: DataSource %s", dbg_to_string);
#endif
    }
    
    public override void notify_membership_changed(DataCollection? collection) {
        // DataSources can only be removed once they've been destroyed or unlinked.
        if (collection == null) {
            assert(is_destroyed || backlinks != null);
        } else {
            assert(!is_destroyed);
        }
        
        // If removed from a collection but have backlinks, then that's an unlink.
        if (collection == null && backlinks != null)
            notify_unlinked();
        
        base.notify_membership_changed(collection);
    }
    
    public virtual void notify_held_in_tank(SourceHoldingTank? holding_tank) {
        // this should never be called if part of a collection
        assert(get_membership() == null);
        
        // DataSources can only be held in a tank if not already in one, and must be removed from
        // one before being put in another
        if (holding_tank != null) {
            assert(this.holding_tank == null);
        } else {
            assert(this.holding_tank != null);
        }
        
        this.holding_tank = holding_tank;
    }
    
    public override void notify_altered(Alteration alteration) {
        // re-route this to the SourceHoldingTank if held in one
        if (holding_tank != null) {
            holding_tank.internal_notify_altered(this, alteration);
        } else {
            contact_subscribers_alteration(alteration);
            
            base.notify_altered(alteration);
        }
    }
    
    // This method is called by SourceCollection.  It should not be called otherwise.
    public virtual void notify_unlinking(SourceCollection collection) {
        assert(backlinks == null && unlinked_from_collection == null);
        
        unlinked_from_collection = collection;
        backlinks = new Gee.HashMap<string, Gee.List<string>>();
    }
    
    // This method is called by DataSource.  It should not be called otherwise.
    protected virtual void notify_unlinked() {
        assert(unlinked_from_collection != null && backlinks != null);
        
        unlinked(unlinked_from_collection);
        
        // give the DataSource a chance to persist the link state, if any
        if (backlinks.size > 0)
            commit_backlinks(unlinked_from_collection, dehydrate_backlinks());
    }
    
    // This method is called by SourceCollection.  It should not be called otherwise.
    public virtual void notify_relinking(SourceCollection collection) {
        assert((backlinks != null) && (unlinked_from_collection == collection));
    }
    
    // This method is called by SourceCollection.  It should not be called otherwise.
    public virtual void notify_relinked() {
        assert(backlinks != null && unlinked_from_collection != null);
        
        SourceCollection relinked_to = unlinked_from_collection;
        backlinks = null;
        unlinked_from_collection = null;
        relinked(relinked_to);
        
        // have the DataSource delete any persisted link state
        commit_backlinks(null, null);
    }
    
    // Each DataSource has a unique typename.  All DataSources of the same type should have the
    // same typename.  This method should be thread-safe.
    //
    // NOTE: Because this value may be persisted in various ways, it should not be changed once
    // defined.
    public abstract string get_typename();
    
    // Each DataSource of a particular typename has an instance ID.  Many DataSources can have a
    // typename of "tag" and many DataSources can have an ID of 42, but only one DataSource may
    // have a typename of "tag" AND an ID of 42.  If the DataSource is persisted, this number should
    // be persisted as well.  This method should be thread-safe.
    public abstract int64 get_instance_id();
    
    // This returns a string that can be used to uniquely identify the DataSource throughout the
    // system.  This method should be thread-safe.
    public virtual string get_source_id() {
        return ("%s-%016" + int64.FORMAT_MODIFIER + "x").printf(get_typename(), get_instance_id());
    }
    
    public bool has_backlink(SourceBacklink backlink) {
        if (backlinks == null)
            return false;
        
        Gee.List<string>? values = backlinks.get(backlink.name);
        
        return values != null ? values.contains(backlink.value) : false;
    }
    
    public Gee.List<SourceBacklink>? get_backlinks(string name) {
        if (backlinks == null)
            return null;
        
        Gee.List<string>? values = backlinks.get(name);
        if (values == null || values.size == 0)
            return null;
        
        Gee.List<SourceBacklink> backlinks = new Gee.ArrayList<SourceBacklink>();
        foreach (string value in values)
            backlinks.add(new SourceBacklink(name, value));
        
        return backlinks;
    }
    
    public void set_backlink(SourceBacklink backlink) {
        // can only be called during an unlink operation
        assert(backlinks != null);
        
        Gee.List<string> values = backlinks.get(backlink.name);
        if (values == null) {
            values = new Gee.ArrayList<string>();
            backlinks.set(backlink.name, values);
        }
        
        values.add(backlink.value);
        
        SourceCollection? sources = (SourceCollection?) get_membership();
        if (sources != null)
            sources.internal_backlink_set(this, backlink);
    }
    
    public bool remove_backlink(SourceBacklink backlink) {
        if (backlinks == null)
            return false;
        
        Gee.List<string> values = backlinks.get(backlink.name);
        if (values == null)
            return false;
        
        int original_size = values.size;
        assert(original_size > 0);
        
        Gee.Iterator<string> iter = values.iterator();
        while (iter.next()) {
            if (iter.get() == backlink.value)
                iter.remove();
        }
        
        if (values.size == 0)
            backlinks.unset(backlink.name);
        
        // Commit here because this can come at any time; setting the backlinks should only 
        // happen during an unlink, which commits at the end of the cycle.
        commit_backlinks(unlinked_from_collection, dehydrate_backlinks());
        
        SourceCollection? sources = (SourceCollection?) get_membership();
        if (sources != null)
            sources.internal_backlink_removed(this, backlink);
        
        return values.size != original_size;
    }
    
    // Base implementation is to do nothing; if DataSource wishes to persist link state across
    // application sessions, it should do so when this is called.  Do not call this base method
    // when overriding; it will only issue a warning.
    //
    // If dehydrated is null, the persisted link state should be deleted.  sources will be null
    // as well.
    protected virtual void commit_backlinks(SourceCollection? sources, string? dehydrated) {
        if (sources != null || dehydrated != null)
            warning("No implementation to commit link state for %s", to_string());
    }
    
    private string? dehydrate_backlinks() {
        if (backlinks == null || backlinks.size == 0)
            return null;
        
        StringBuilder builder = new StringBuilder();
        foreach (string name in backlinks.keys) {
            Gee.List<string> values = backlinks.get(name);
            if (values == null || values.size == 0)
                continue;
            
            string value_field = "";
            foreach (string value in values) {
                if (!is_string_empty(value))
                    value_field += value + "|";
            }
            
            if (value_field.length > 0)
                builder.append("%s=%s\n".printf(name, value_field));
        }
        
        return builder.str.length > 0 ? builder.str : null;
    }
    
    // If dehydrated is null, this method will still put the DataSource into an unlinked state,
    // simply without any backlinks to reestablish.
    public void rehydrate_backlinks(SourceCollection unlinked_from, string? dehydrated) {
        unlinked_from_collection = unlinked_from;
        backlinks = new Gee.HashMap<string, Gee.List<string>>();
        
        if (dehydrated == null)
            return;
        
        string[] lines = dehydrated.split("\n");
        foreach (string line in lines) {
            if (line.length == 0)
                continue;
            
            string[] tokens = line.split("=", 2);
            if (tokens.length < 2) {
                warning("Unable to rehydrate \"%s\" for %s: name and value not present", line,
                    to_string());
                
                continue;
            }
            
            string[] decoded_values = tokens[1].split("|");
            Gee.List<string> values = new Gee.ArrayList<string>();
            foreach (string value in decoded_values) {
                if (value != null && value.length > 0)
                    values.add(value);
            }
            
            if (values.size > 0)
                backlinks.set(tokens[0], values);
        }
    }
    
    // If a DataSource cannot produce snapshots, return null.
    public virtual SourceSnapshot? save_snapshot() {
        return null;
    }
    
    // This method is called by SourceCollection.  It should not be called otherwise.
    public void internal_mark_for_destroy() {
        marked_for_destroy = true;
    }
    
    // This method is called by SourceCollection.  It should not be called otherwise.
    //
    // This method deletes whatever backing this DataSource represents.  It should either return
    // false or throw an error if the delete fails.
    public virtual bool internal_delete_backing() throws Error {
        return true;
    }
    
    // Because of the rules of DataSources, a DataSource is only equal to itself; subclasses
    // may override this to perform validations and/or assertions
    public virtual bool equals(DataSource? source) {
        return (this == source);
    }
    
    // This method is called by SourceCollection.  It should not be called otherwise.  To destroy
    // a DataSource, destroy it from its SourceCollection.
    //
    // Child classes should call this base class to ensure that the collection this object is
    // a member of is notified and the signal is properly called.  The collection will remove this
    // object automatically.
    public virtual void destroy() {
        assert(marked_for_destroy);
        
        // mark as destroyed
        is_destroyed = true;
        
        // unsubscribe all subscribers
        for (int ctr = 0; ctr < subscribers.length; ctr++) {
            if (subscribers[ctr] != null) {
                DataView view = subscribers[ctr];
                subscribers[ctr] = null;
                
                view.notify_unsubscribed(this);
            }
        }
        
        // propagate the signal
        destroyed();
    }
    
    // This method can be used to destroy a DataSource before it's added to a SourceCollection
    // or has been unlinked from one. It should not be used otherwise.  (In particular, don't
    // automate destroys by removing and then calling this method -- that will happen automatically.)
    // To destroy a DataSource already integrated into a SourceCollection, call
    // SourceCollection.destroy_marked().  Returns true if the operation completed successfully,
    // otherwise it will return false.
    public bool destroy_orphan(bool delete_backing) {
        bool ret = true;
        if (delete_backing) {
            try {
                ret = internal_delete_backing();
                if (!ret)
                    warning("Unable to delete backing for %s", to_string());
                    
            } catch (Error err) {
                warning("Unable to delete backing for %s: %s", to_string(), err.message);
                ret = false;
            }
        }
        
        internal_mark_for_destroy();
        destroy();
        
        if (unlinked_from_collection != null)
            unlinked_from_collection.notify_unlinked_destroyed(this);
            
        return ret;
    }

    // DataViews subscribe to the DataSource to inform it of their existence.  Not only does this
    // allow for signal reflection (i.e. DataSource.altered -> DataView.altered) it also makes
    // them first-in-line for notification of destruction, so they can remove themselves from 
    // their ViewCollections automatically.
    //
    // This method is only called by DataView.
    public void internal_subscribe(DataView view) {
        assert(!in_contact);
        
        for (int ctr = 0; ctr < subscribers.length; ctr++) {
            if (subscribers[ctr] == null) {
                subscribers[ctr] = view;
                
                return;
            }
        }
        
        subscribers += view;
    }
    
    // This method is only called by DataView.  NOTE: This method does NOT call
    // DataView.notify_unsubscribed(), as it's assumed the DataView itself will do so if appropriate.
    public void internal_unsubscribe(DataView view) {
        assert(!in_contact);
        
        for (int ctr = 0; ctr < subscribers.length; ctr++) {
            if (subscribers[ctr] == view) {
                subscribers[ctr] = null;
                
                return;
            }
        }
    }
    
    protected void contact_subscribers(ContactSubscriber contact_subscriber) {
        assert(!in_contact);
        
        in_contact = true;
        for (int ctr = 0; ctr < subscribers.length; ctr++) {
            if (subscribers[ctr] != null)
                contact_subscriber(subscribers[ctr]);
        }
        in_contact = false;
    }
    
    protected void contact_subscribers_alteration(Alteration alteration) {
        assert(!in_contact);
        
        in_contact = true;
        for (int ctr = 0; ctr < subscribers.length; ctr++) {
            if (subscribers[ctr] != null)
                subscribers[ctr].notify_altered(alteration);
        }
        in_contact = false;
    }
}

public abstract class SourceSnapshot {
    private bool snapshot_broken = false;
    
    // This is signalled when the DataSource, for whatever reason, can no longer be reconstituted
    // from this Snapshot.
    public virtual signal void broken() {
    }
    
    public virtual void notify_broken() {
        snapshot_broken = true;
        
        broken();
    }
    
    public bool is_broken() {
        return snapshot_broken;
    }
}

// Link state name may not contain the equal sign ("=").  Link names and values may not contain the 
// pipe-character ("|").  Both will be stripped of leading and trailing whitespace.  This may
// affect retrieval.
public class SourceBacklink {
    private string _name;
    private string _value;
    
    public string name {
        get {
            return _name;
        }
    }
    
    public string value {
        get {
            return _value;
        }
    }
    
    // This only applies if the SourceBacklink comes from a DataSource.
    public string typename {
        get {
            return _name;
        }
    }
    
    // This only applies if the SourceBacklink comes from a DataSource.
    public int64 instance_id {
        get {
            return int64.parse(_value);
        }
    }
    
    public SourceBacklink(string name, string value) {
        assert(validate_name_value(name, value));
        
        _name = name.strip();
        _value = value.strip();
    }
    
    public SourceBacklink.from_source(DataSource source) {
        _name = source.get_typename().strip();
        _value = source.get_instance_id().to_string().strip();
        
        assert(validate_name_value(_name, _value));
    }
    
    private static bool validate_name_value(string name, string value) {
        return !name.contains("=") && !name.contains("|") && !value.contains("|");
    }
    
    public string to_string() {
        return "Backlink %s=%s".printf(name, value);
    }
    
    public static uint hash_func(SourceBacklink? backlink) {        
        return str_hash(backlink._name) ^ str_hash(backlink._value);
    }
    
    public static bool equal_func(SourceBacklink? alink, SourceBacklink? blink) {       
        return str_equal(alink._name, blink._name) && str_equal(alink._value, blink._value);
    }
}

//
// SourceProxy
//
// A SourceProxy allows for a DataSource's state to be maintained in memory regardless of
// whether or not the DataSource has been destroyed.  If a user of SourceProxy
// requests the represented object and it is still in memory, it will be returned.  If not, it
// is reconstituted and the new DataSource is returned.
//
// Several SourceProxy can be wrapped around the same DataSource.  If the DataSource is
// destroyed, all Proxys drop their reference.  When a Proxy reconstitutes the DataSource, all
// will be aware of it and re-establish their reference.
//
// The snapshot that is maintained is the snapshot in regards to the time of the Proxy's creation.
// Proxys do not update their snapshot thereafter.  If a snapshot reports it is broken, the
// Proxy will not reconstitute the DataSource and get_source() will return null thereafter.
//
// There is no preferential treatment in regards to snapshots of the DataSources.  The first
// Proxy to reconstitute the DataSource wins.
//

public abstract class SourceProxy {
    private int64 object_id;
    private string source_string;
    private DataSource source;
    private SourceSnapshot snapshot;
    private SourceCollection membership;
    
    // This is only signalled by the SourceProxy that reconstituted the DataSource.  All
    // Proxys will signal when this occurs.
    public virtual signal void reconstituted(DataSource source) {
    }
    
    // This is signalled when the SourceProxy has dropped a destroyed DataSource.  Calling
    // get_source() will force it to be reconstituted.
    public virtual signal void dehydrated() {
    }
    
    // This is signalled when the held DataSourceSnapshot reports it is broken.  The DataSource
    // will not be reconstituted and get_source() will return null thereafter.
    public virtual signal void broken() {
    }
    
    public SourceProxy(DataSource source) {
        object_id = source.get_object_id();
        source_string = source.to_string();
        
        snapshot = source.save_snapshot();
        assert(snapshot != null);
        snapshot.broken.connect(on_snapshot_broken);
        
        set_source(source);
        
        membership = (SourceCollection) source.get_membership();
        assert(membership != null);
        membership.items_added.connect(on_source_added);
    }
    
    ~SourceProxy() {
        drop_source();
        membership.items_added.disconnect(on_source_added);
    }
    
    public abstract DataSource reconstitute(int64 object_id, SourceSnapshot snapshot);
    
    public virtual void notify_reconstituted(DataSource source) {
        reconstituted(source);
    }
    
    public virtual void notify_dehydrated() {
        dehydrated();
    }
    
    public virtual void notify_broken() {
        broken();
    }
    
    private void on_snapshot_broken() {
        drop_source();
        
        notify_broken();
    }
    
    private void set_source(DataSource source) {
        drop_source();
        
        this.source = source;
        source.destroyed.connect(on_destroyed);
    }
    
    private void drop_source() {
        if (source == null)
            return;
        
        source.destroyed.disconnect(on_destroyed);
        source = null;
    }
    
    public DataSource? get_source() {
        if (snapshot.is_broken())
            return null;
        
        if (source != null)
            return source;
        
        // without the source, need to reconstitute it and re-add to its original SourceCollection
        // it should also automatically add itself to its original collection (which is trapped
        // in on_source_added)
        DataSource new_source = reconstitute(object_id, snapshot);
        if (source != new_source)
            source = new_source;
        if (object_id != source.get_object_id())
            object_id = new_source.get_object_id();
        assert(source.get_object_id() == object_id);
        assert(membership.contains(source));
        
        return source;
    }
    
    private void on_destroyed() {
        assert(source != null);
        
        // drop the reference ... will need to reconstitute later if requested
        drop_source();
        
        notify_dehydrated();
    }
    
    private void on_source_added(Gee.Iterable<DataObject> added) {
        // only interested in new objects when the proxied object has gone away
        if (source != null)
            return;
        
        foreach (DataObject object in added) {
            // looking for new objects with original source object's id
            if (object.get_object_id() != object_id)
                continue;
            
            // this is it; stash for future use
            set_source((DataSource) object);
            
            notify_reconstituted((DataSource) object);
            
            break;
        }
    }
}

public interface Proxyable : Object {
    public abstract SourceProxy get_proxy();
}