summaryrefslogtreecommitdiff
path: root/src/core/DataSource.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/DataSource.vala')
-rw-r--r--src/core/DataSource.vala679
1 files changed, 679 insertions, 0 deletions
diff --git a/src/core/DataSource.vala b/src/core/DataSource.vala
new file mode 100644
index 0000000..e4d2d34
--- /dev/null
+++ b/src/core/DataSource.vala
@@ -0,0 +1,679 @@
+/* Copyright 2011-2014 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();
+}
+