2 * Copyright (C) 2011 Collabora Ltd.
4 * This library is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 2.1 of the License, or
7 * (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this library. If not, see <http://www.gnu.org/licenses/>.
18 * Philip Withnall <philip.withnall@collabora.co.uk>
25 * A generic abstract cache for sets of objects. This can be used by subclasses
26 * to implement caching of homogeneous sets of objects. Subclasses simply have
27 * to implement serialisation and deserialisation of the objects to and from
28 * {@link GLib.Variant}s.
30 * It's intended that this class be used for providing caching layers for
31 * {@link PersonaStore}s, for example.
35 public abstract class Folks.ObjectCache<T> : Object
37 /* The version number of the header/wrapper for a cache file. When accompanied
38 * by a version number for the serialised object type, this unambiguously
39 * keys the variant type describing an entire cache file.
41 * The wrapper and object version numbers are stored as the first two bytes
42 * of a cache file. They can't be stored as part of the Variant which forms
43 * the rest of the file, as to interpret the Variant its entire type has to
44 * be known — which depends on the version numbers. */
45 private static const uint8 _FILE_FORMAT_VERSION = 1;
47 /* The length of the version header at the beginning of the file. This has
48 * to be a multiple of 8 to keep Variant's alignment code happy.
49 * As documented above, currently only the first two bytes of this header
50 * are used (for version numbers). */
51 private static const size_t _HEADER_WIDTH = 8; /* bytes */
53 private File _cache_directory;
54 private File _cache_file;
55 private string _cache_file_path; /* save calls to _cache_file.get_path() */
58 * Get the {@link GLib.VariantType} of the serialised form of an object stored
61 * If a smooth upgrade path is needed in future due to cache file format
62 * changes, this may be modified to take a version parameter.
64 * @param object_version the version of the object format to use, or
65 * `uint8.MAX` for the latest version
66 * @return variant type for that object version, or `null` if the version is
70 protected abstract VariantType? get_serialised_object_type (
71 uint8 object_version);
74 * Get the version of the variant type returned by
75 * {@link ObjectCache.get_serialised_object_type}. This must be incremented
76 * every time the variant type changes so that old cache files aren't
81 protected abstract uint8 get_serialised_object_version ();
84 * Serialise the given `object` to a {@link GLib.Variant} and return the
85 * variant. The variant must be of the type returned by
86 * {@link ObjectCache.get_serialised_object_type}.
88 * @param object the object to serialise
89 * @return serialised form of `object`
93 protected abstract Variant serialise_object (T object);
96 * Deserialise the given `variant` to a new instance of an object. The variant
97 * is guaranteed to have the type returned by
98 * {@link ObjectCache.get_serialised_object_type}.
100 * @param variant the serialised form to deserialise
101 * @param object_version the version of the object format to deserialise from
102 * @return the deserialised object
106 protected abstract T deserialise_object (Variant variant,
107 uint8 object_version);
110 * A string identifying the type of object being cached.
112 * This has to be suitable for use as a directory name; i.e. lower case,
113 * hyphen-separated tokens.
117 public string type_id { get; construct; }
120 * A string identifying the particular cache instance.
122 * This will form the file name of the cache file, but will be escaped
123 * beforehand, so can be an arbitrary non-empty string.
129 get { return this._id; }
130 construct { assert (value != ""); this._id = value; }
135 * Create a new cache instance using the given type ID and ID. This is
136 * protected as the `type_id` will typically be set statically by subclasses.
138 * @param type_id A string identifying the type of object being cached. This
139 * has to be suitable for use as a directory name; i.e. lower case,
141 * @param id A string identifying the particular cache instance. This will
142 * form the file name of the cache file, but will be escaped beforehand, so
143 * can be an arbitrary non-empty string.
144 * @return A new cache instance
148 protected ObjectCache (string type_id, string id)
150 Object (type_id: type_id,
156 debug ("Creating object cache for type ID '%s' with ID '%s'.",
157 this.type_id, this.id);
159 this._cache_directory =
160 File.new_for_path (Environment.get_user_cache_dir ())
162 .get_child (this.type_id);
164 this._cache_directory.get_child (Uri.escape_string (this.id,
166 var path = this._cache_file.get_path ();
167 this._cache_file_path = (path != null) ? (!) path : "(null)";
171 * Load a set of objects from the cache and return them as a new set. If the
172 * cache file doesn't exist, `null` will be returned. An empty set will be
173 * returned if the cache file existed but was empty (i.e. was stored with
174 * an empty set originally).
176 * Loading the objects can be cancelled using `cancellable`. If it is, `null`
179 * If any errors are encountered while loading the objects, warnings will be
180 * logged as appropriate and `null` will be returned.
182 * @param cancellable A {@link GLib.Cancellable} for the operation, or `null`.
183 * @return A set of objects from the cache, or `null`.
187 public async Set<T>? load_objects (Cancellable? cancellable = null)
189 debug ("Loading cache (type ID '%s', ID '%s') from file '%s'.",
190 this.type_id, this._id, this._cache_file_path);
197 yield this._cache_file.load_contents_async (cancellable, out data, null);
201 if (e is IOError.CANCELLED)
203 /* not a true error */
205 else if (e is IOError.NOT_FOUND)
207 debug ("Couldn't load cache file '%s': %s",
208 this._cache_file_path, e.message);
212 warning ("Couldn't load cache file '%s': %s",
213 this._cache_file_path, e.message);
220 if (data.length < this._HEADER_WIDTH)
222 warning ("Cache file '%s' was too small. The file was deleted.",
223 this._cache_file_path);
224 yield this.clear_cache ();
230 var wrapper_version = data[0];
231 var object_version = data[1];
233 if (wrapper_version != this._FILE_FORMAT_VERSION)
235 warning ("Cache file '%s' was version %u of the file format, " +
236 "but only version %u is supported. The file was deleted.",
237 this._cache_file_path, wrapper_version,
238 this._FILE_FORMAT_VERSION);
239 yield this.clear_cache ();
244 unowned uint8[] variant_data = data[this._HEADER_WIDTH:data.length];
246 // Deserialise the variant according to the given version numbers
248 this._get_cache_file_variant_type (wrapper_version, object_version);
250 if (_variant_type == null)
252 warning ("Cache file '%s' was version %u of the object file " +
253 "format, which is not supported. The file was deleted.",
254 this._cache_file_path, object_version,
255 this._FILE_FORMAT_VERSION);
256 yield this.clear_cache ();
260 var variant_type = (!) _variant_type;
263 Variant.new_from_data<uint8[]> (variant_type, variant_data, false,
266 // Check the variant was deserialised correctly
267 if (variant.is_normal_form () == false)
269 warning ("Cache file '%s' was corrupt and was deleted.",
270 this._cache_file_path);
271 yield this.clear_cache ();
276 // Unpack the stored data
277 var type_id = variant.get_child_value (0).get_string ();
279 if (type_id != this.type_id)
281 warning ("Cache file '%s' had type ID '%s', but '%s' was expected." +
282 "The file was deleted.", this._cache_file_path, type_id,
284 yield this.clear_cache ();
289 var id = variant.get_child_value (1).get_string ();
293 warning ("Cache file '%s' had ID '%s', but '%s' was expected." +
294 "The file was deleted.", this._cache_file_path, id,
296 yield this.clear_cache ();
301 var objects_variant = variant.get_child_value (2);
303 var objects = new HashSet<T> ();
305 for (uint i = 0; i < objects_variant.n_children (); i++)
307 var object_variant = objects_variant.get_child_value (i);
308 var object = this.deserialise_object (object_variant, object_version);
310 objects.add (object);
317 * Store a set of objects to the cache file, overwriting any existing set of
318 * objects in the cache, or creating the cache file from scratch if it didn't
321 * Storing the objects can be cancelled using `cancellable`. If it is, the
322 * cache will be left in a consistent state, but may be storing the old set
323 * of objects or the new set.
325 * @param objects A set of objects to store. This may be empty, but may not
327 * @param cancellable A {@link GLib.Cancellable} for the operation, or `null`.
331 public async void store_objects (Set<T> objects,
332 Cancellable? cancellable = null)
334 debug ("Storing cache (type ID '%s', ID '%s') to file '%s'.",
335 this.type_id, this._id, this._cache_file_path);
337 var child_type = this.get_serialised_object_type (uint8.MAX);
338 assert (child_type != null); // uint8.MAX should always be supported
339 Variant[] children = new Variant[objects.size];
341 // Serialise all the objects in the set
343 foreach (var object in objects)
345 children[i++] = this.serialise_object (object);
349 var wrapper_version = this._FILE_FORMAT_VERSION;
350 var object_version = this.get_serialised_object_version ();
352 var variant = new Variant.tuple ({
353 new Variant.string (this.type_id), // Type ID
354 new Variant.string (this._id), // ID
355 new Variant.array (child_type, children) // Array of objects
358 var desired_variant_type =
359 this._get_cache_file_variant_type (wrapper_version, object_version);
360 assert (desired_variant_type != null &&
361 variant.get_type ().equal ((!) desired_variant_type));
363 // Prepend the version numbers to the data
364 uint8[] data = new uint8[this._HEADER_WIDTH + variant.get_size ()];
365 data[0] = wrapper_version;
366 data[1] = object_version;
367 variant.store (data[this._HEADER_WIDTH:data.length]);
369 // Write the data out to the file
374 yield this._cache_file.replace_contents_async (
376 FileCreateFlags.PRIVATE, cancellable, null);
381 if (e is IOError.NOT_FOUND)
385 yield this._create_cache_directory ();
390 warning ("Couldn't create cache directory '%s': %s",
391 this._cache_directory.get_path (), e.message);
395 else if (e is IOError.CANCELLED)
397 /* We assume the replace_contents_async() call is atomic,
398 * so cancelling it is atomic as well. */
402 /* Print a warning and delete the cache file so we don't leave
403 * stale cached objects lying around. */
404 warning ("Couldn't write to cache file '%s', so deleting it: %s",
405 this._cache_file_path, e.message);
406 yield this.clear_cache ();
414 * Clear this cache object, deleting its backing file.
418 public async void clear_cache ()
420 debug ("Clearing cache (type ID '%s', ID '%s'); deleting file '%s'.",
421 this.type_id, this._id, this._cache_file_path);
425 this._cache_file.delete ();
433 private VariantType? _get_cache_file_variant_type (uint8 wrapper_version,
434 uint8 object_version)
436 var _object_type = this.get_serialised_object_type (object_version);
438 if (_object_type == null)
440 // Unsupported version
443 var object_type = (!) _object_type;
445 return new VariantType.tuple ({
446 VariantType.STRING, // Type ID
447 VariantType.STRING, // ID
448 new VariantType.array (object_type) // Objects
452 private async void _create_cache_directory () throws Error
456 this._cache_directory.make_directory_with_parents ();
460 // Ignore errors caused by the directory existing already
461 if (!(e is IOError.EXISTS))
469 /* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */