/* 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. */ // // Alteration represents a description of what has changed in the DataObject (reported via the // "altered" signal). Since the descriptions can vary wildly depending on the semantics of each // DataObject, no assumptions or requirements are placed on Alteration other than it must have // one or more "subjects", each with a "detail". Subscribers to the "altered" signal can query // the Alteration object to determine if the change is important to them. // // Alteration is an immutable type. This means it's possible to store const Alterations of oft-used // values for reuse. // // Alterations may be compressed, merging their subjects and details into a new aggregated // Alteration. Generally this is handled automatically by DataObject and DataCollection, when // necessary. // // NOTE: subjects and details should be ASCII labels (as in, plain-old ASCII, no code pages). // They are treated as case-sensitive strings. // // Recommended subjects include: image, thumbnail, metadata. // public class Alteration { private string subject = null; private string detail = null; private Gee.MultiMap map = null; public Alteration(string subject, string detail) { add_detail(subject, detail); } // Create an Alteration that has more than one subject/detail. list is a comma-delimited // string of colon-separated subject:detail pairs. public Alteration.from_list(string list) requires (list.length > 0) { string[] pairs = list.split(","); assert(pairs.length >= 1); foreach (string pair in pairs) { string[] subject_detail = pair.split(":", 2); assert(subject_detail.length == 2); add_detail(subject_detail[0], subject_detail[1]); } } // Create an Alteration that has more than one subject/detail from an array of comma-delimited // strings of colon-separate subject:detail pairs public Alteration.from_array(string[] array) requires (array.length > 0) { foreach (string pair in array) { string[] subject_detail = pair.split(":", 2); assert(subject_detail.length == 2); add_detail(subject_detail[0], subject_detail[1]); } } // Used for compression. private Alteration.from_map(Gee.MultiMap map) { this.map = map; } private void add_detail(string sub, string det) { // strip leading and trailing whitespace string subject = sub.strip(); assert(subject.length > 0); string detail = det.strip(); assert(detail.length > 0); // if a simple Alteration, store in singleton refs if (this.subject == null && map == null) { assert(this.detail == null); this.subject = subject; this.detail = detail; return; } // Now a complex Alteration, requiring a Map. if (map == null) map = create_map(); // Move singletons into Map if (this.subject != null) { assert(this.detail != null); map.set(this.subject, this.detail); this.subject = null; this.detail = null; } // Store new subject:detail in Map as well map.set(subject, detail); } private Gee.MultiMap create_map() { return new Gee.HashMultiMap(case_hash, case_equal, case_hash, case_equal); } private static bool case_equal(string? a, string? b) { return equal_values(a, b); } private static uint case_hash(string? a) { return hash_value(a); } private static inline bool equal_values(string str1, string str2) { return str1.ascii_casecmp(str2) == 0; } private static inline uint hash_value(string str) { return str_hash(str); } public bool has_subject(string subject) { if (this.subject != null) return equal_values(this.subject, subject); assert(map != null); Gee.Set? keys = map.get_keys(); if (keys != null) { foreach (string key in keys) { if (equal_values(key, subject)) return true; } } return false; } public bool has_detail(string subject, string detail) { if (this.subject != null && this.detail != null) return equal_values(this.subject, subject) && equal_values(this.detail, detail); assert(map != null); Gee.Collection? values = map.get(subject); if (values != null) { foreach (string value in values) { if (equal_values(value, detail)) return true; } } return false; } public Gee.Collection? get_details(string subject) { if (this.subject != null && detail != null && equal_values(this.subject, subject)) { Gee.ArrayList details = new Gee.ArrayList(); details.add(detail); return details; } return (map != null) ? map.get(subject) : null; } public string to_string() { if (subject != null) { assert(detail != null); return "%s:%s".printf(subject, detail); } assert(map != null); string str = ""; foreach (string key in map.get_keys()) { foreach (string value in map.get(key)) { if (str.length != 0) str += ", "; str += "%s:%s".printf(key, value); } } return str; } // Returns true if this object has any subject:detail matches with the supplied Alteration. public bool contains_any(Alteration other) { // identity if (this == other) return true; // if both singletons, check for singleton match if (subject != null && other.subject != null && detail != null && other.detail != null) return equal_values(subject, other.subject) && equal_values(detail, other.detail); // if one is singleton and the other a multiple, search for singleton in multiple if ((map != null && other.map == null) || (map == null && other.map != null)) { string single_subject = subject != null ? subject : other.subject; string single_detail = detail != null ? detail : other.detail; Gee.MultiMap multimap = map != null ? map : other.map; return multimap.contains(single_subject) && map.get(single_subject).contains(single_detail); } // if both multiples, check for any match at all if (map != null && other.map != null) { Gee.Set? keys = map.get_keys(); assert(keys != null); Gee.Set? other_keys = other.map.get_keys(); assert(other_keys != null); foreach (string subject in other_keys) { if (!keys.contains(subject)) continue; Gee.Collection? details = map.get(subject); Gee.Collection? other_details = other.map.get(subject); if (details != null && other_details != null) { foreach (string detail in other_details) { if (details.contains(detail)) return true; } } } } return false; } public bool equals(Alteration other) { // identity if (this == other) return true; // if both singletons, check for singleton match if (subject != null && other.subject != null && detail != null && other.detail != null) return equal_values(subject, other.subject) && equal_values(detail, other.detail); // if both multiples, check for across-the-board matches if (map != null && other.map != null) { // see if both maps contain the same set of keys Gee.Set? keys = map.get_keys(); assert(keys != null); Gee.Set? other_keys = other.map.get_keys(); assert(other_keys != null); if (keys.size != other_keys.size) return false; if (!keys.contains_all(other_keys)) return false; if (!other_keys.contains_all(keys)) return false; foreach (string key in keys) { Gee.Collection values = map.get(key); Gee.Collection other_values = other.map.get(key); if (values.size != other_values.size) return false; if (!values.contains_all(other_values)) return false; if (!other_values.contains_all(values)) return false; } // maps are identical return true; } // one singleton and one multiple, not equal return false; } private static void multimap_add_all(Gee.MultiMap dest, Gee.MultiMap src) { Gee.Set keys = src.get_keys(); foreach (string key in keys) { Gee.Collection values = src.get(key); foreach (string value in values) dest.set(key, value); } } // This merges the Alterations, returning a new Alteration with both represented. If both // Alterations are equal, this will return this object rather than create a new one. public Alteration compress(Alteration other) { if (equals(other)) return this; // Build a new Alteration with both represented ... if they're unequal, then the new one // is guaranteed not to be a singleton Gee.MultiMap compressed = create_map(); if (subject != null && detail != null) { compressed.set(subject, detail); } else { assert(map != null); multimap_add_all(compressed, map); } if (other.subject != null && other.detail != null) { compressed.set(other.subject, other.detail); } else { assert(other.map != null); multimap_add_all(compressed, other.map); } return new Alteration.from_map(compressed); } }