Post-release version bump
[platform/upstream/folks.git] / folks / object-cache.vala
1 /*
2  * Copyright (C) 2011 Collabora Ltd.
3  *
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.
8  *
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.
13  *
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/>.
16  *
17  * Authors:
18  *       Philip Withnall <philip.withnall@collabora.co.uk>
19  */
20
21 using GLib;
22 using Gee;
23
24 /**
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.
29  *
30  * It's intended that this class be used for providing caching layers for
31  * {@link PersonaStore}s, for example.
32  *
33  * @since 0.6.0
34  */
35 public abstract class Folks.ObjectCache<T> : Object
36 {
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.
40    *
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;
46
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 */
52
53   private File _cache_directory;
54   private File _cache_file;
55   private string _cache_file_path; /* save calls to _cache_file.get_path() */
56
57   /**
58    * Get the {@link GLib.VariantType} of the serialised form of an object stored
59    * in this cache.
60    *
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.
63    *
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
67    * unsupported
68    * @since 0.6.0
69    */
70   protected abstract VariantType? get_serialised_object_type (
71       uint8 object_version);
72
73   /**
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
77    * misinterpreted.
78    *
79    * @since 0.6.0
80    */
81   protected abstract uint8 get_serialised_object_version ();
82
83   /**
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}.
87    *
88    * @param object the object to serialise
89    * @return serialised form of `object`
90    *
91    * @since 0.6.0
92    */
93   protected abstract Variant serialise_object (T object);
94
95   /**
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}.
99    *
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
103    *
104    * @since 0.6.0
105    */
106   protected abstract T deserialise_object (Variant variant,
107       uint8 object_version);
108
109   /**
110    * A string identifying the type of object being cached.
111    *
112    * This has to be suitable for use as a directory name; i.e. lower case,
113    * hyphen-separated tokens.
114    *
115    * @since 0.6.6
116    */
117   public string type_id { get; construct; }
118
119   /**
120    * A string identifying the particular cache instance.
121    *
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.
124    *
125    * @since 0.6.6
126    */
127   public string id
128     {
129       get { return this._id; }
130       construct { assert (value != ""); this._id = value; }
131     }
132   private string _id;
133
134   /**
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.
137    *
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,
140    * hyphen-separated.
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
145    *
146    * @since 0.6.0
147    */
148   protected ObjectCache (string type_id, string id)
149     {
150       Object (type_id: type_id,
151               id: id);
152     }
153
154   construct
155     {
156       debug ("Creating object cache for type ID '%s' with ID '%s'.",
157           this.type_id, this.id);
158
159       this._cache_directory =
160           File.new_for_path (Environment.get_user_cache_dir ())
161               .get_child ("folks")
162               .get_child (this.type_id);
163       this._cache_file =
164           this._cache_directory.get_child (Uri.escape_string (this.id,
165               "", false));
166       var path = this._cache_file.get_path ();
167       this._cache_file_path = (path != null) ? (!) path : "(null)";
168     }
169
170   /**
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).
175    *
176    * Loading the objects can be cancelled using `cancellable`. If it is, `null`
177    * will be returned.
178    *
179    * If any errors are encountered while loading the objects, warnings will be
180    * logged as appropriate and `null` will be returned.
181    *
182    * @param cancellable A {@link GLib.Cancellable} for the operation, or `null`.
183    * @return A set of objects from the cache, or `null`.
184    *
185    * @since 0.6.0
186    */
187   public async Set<T>? load_objects (Cancellable? cancellable = null)
188     {
189       debug ("Loading cache (type ID '%s', ID '%s') from file '%s'.",
190           this.type_id, this._id, this._cache_file_path);
191
192       // Read in the file
193       uint8[] data;
194
195       try
196         {
197           yield this._cache_file.load_contents_async (cancellable, out data, null);
198         }
199       catch (Error e)
200         {
201           if (e is IOError.CANCELLED)
202             {
203               /* not a true error */
204             }
205           else if (e is IOError.NOT_FOUND)
206             {
207               debug ("Couldn't load cache file '%s': %s",
208                   this._cache_file_path, e.message);
209             }
210           else
211             {
212               warning ("Couldn't load cache file '%s': %s",
213                   this._cache_file_path, e.message);
214             }
215
216           return null;
217         }
218
219       // Check the length
220       if (data.length < this._HEADER_WIDTH)
221         {
222           warning ("Cache file '%s' was too small. The file was deleted.",
223               this._cache_file_path);
224           yield this.clear_cache ();
225
226           return null;
227         }
228
229       // Check the version
230       var wrapper_version = data[0];
231       var object_version = data[1];
232
233       if (wrapper_version != this._FILE_FORMAT_VERSION)
234         {
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 ();
240
241           return null;
242         }
243
244       unowned uint8[] variant_data = data[this._HEADER_WIDTH:data.length];
245
246       // Deserialise the variant according to the given version numbers
247       var _variant_type =
248           this._get_cache_file_variant_type (wrapper_version, object_version);
249
250       if (_variant_type == null)
251         {
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 ();
257
258           return null;
259         }
260       var variant_type = (!) _variant_type;
261
262       var variant =
263           Variant.new_from_data<uint8[]> (variant_type, variant_data, false,
264               data);
265
266       // Check the variant was deserialised correctly
267       if (variant.is_normal_form () == false)
268         {
269           warning ("Cache file '%s' was corrupt and was deleted.",
270               this._cache_file_path);
271           yield this.clear_cache ();
272
273           return null;
274         }
275
276       // Unpack the stored data
277       var type_id = variant.get_child_value (0).get_string ();
278
279       if (type_id != this.type_id)
280         {
281           warning ("Cache file '%s' had type ID '%s', but '%s' was expected." +
282               "The file was deleted.", this._cache_file_path, type_id,
283               this.type_id);
284           yield this.clear_cache ();
285
286           return null;
287         }
288
289       var id = variant.get_child_value (1).get_string ();
290
291       if (id != this._id)
292         {
293           warning ("Cache file '%s' had ID '%s', but '%s' was expected." +
294               "The file was deleted.", this._cache_file_path, id,
295               this._id);
296           yield this.clear_cache ();
297
298           return null;
299         }
300
301       var objects_variant = variant.get_child_value (2);
302
303       var objects = new HashSet<T> ();
304
305       for (uint i = 0; i < objects_variant.n_children (); i++)
306         {
307           var object_variant = objects_variant.get_child_value (i);
308           var object = this.deserialise_object (object_variant, object_version);
309
310           objects.add (object);
311         }
312
313       return objects;
314     }
315
316   /**
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
319    * previously exist.
320    *
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.
324    *
325    * @param objects A set of objects to store. This may be empty, but may not
326    * be `null`.
327    * @param cancellable A {@link GLib.Cancellable} for the operation, or `null`.
328    *
329    * @since 0.6.0
330    */
331   public async void store_objects (Set<T> objects,
332       Cancellable? cancellable = null)
333     {
334       debug ("Storing cache (type ID '%s', ID '%s') to file '%s'.",
335           this.type_id, this._id, this._cache_file_path);
336
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];
340
341       // Serialise all the objects in the set
342       uint i = 0;
343       foreach (var object in objects)
344         {
345           children[i++] = this.serialise_object (object);
346         }
347
348       // File format
349       var wrapper_version = this._FILE_FORMAT_VERSION;
350       var object_version = this.get_serialised_object_version ();
351
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
356       });
357
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));
362
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]);
368
369       // Write the data out to the file
370       while (true)
371         {
372           try
373             {
374               yield this._cache_file.replace_contents_async (
375                   data, null, false,
376                   FileCreateFlags.PRIVATE, cancellable, null);
377               break;
378             }
379           catch (Error e)
380             {
381               if (e is IOError.NOT_FOUND)
382                 {
383                   try
384                     {
385                       yield this._create_cache_directory ();
386                       continue;
387                     }
388                   catch (Error e2)
389                     {
390                       warning ("Couldn't create cache directory '%s': %s",
391                           this._cache_directory.get_path (), e.message);
392                       return;
393                     }
394                 }
395               else if (e is IOError.CANCELLED)
396                 {
397                   /* We assume the replace_contents_async() call is atomic,
398                    * so cancelling it is atomic as well. */
399                   return;
400                 }
401
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 ();
407
408               return;
409             }
410         }
411     }
412
413   /**
414    * Clear this cache object, deleting its backing file.
415    *
416    * @since 0.6.0
417    */
418   public async void clear_cache ()
419     {
420       debug ("Clearing cache (type ID '%s', ID '%s'); deleting file '%s'.",
421           this.type_id, this._id, this._cache_file_path);
422
423       try
424         {
425           this._cache_file.delete ();
426         }
427       catch (Error e)
428         {
429           // Ignore errors
430         }
431     }
432
433   private VariantType? _get_cache_file_variant_type (uint8 wrapper_version,
434       uint8 object_version)
435     {
436       var _object_type = this.get_serialised_object_type (object_version);
437
438       if (_object_type == null)
439         {
440           // Unsupported version
441           return null;
442         }
443       var object_type = (!) _object_type;
444
445       return new VariantType.tuple ({
446         VariantType.STRING, // Type ID
447         VariantType.STRING, // ID
448         new VariantType.array (object_type) // Objects
449       });
450     }
451
452   private async void _create_cache_directory () throws Error
453     {
454       try
455         {
456           this._cache_directory.make_directory_with_parents ();
457         }
458       catch (Error e)
459         {
460           // Ignore errors caused by the directory existing already
461           if (!(e is IOError.EXISTS))
462             {
463               throw e;
464             }
465         }
466     }
467 }
468
469 /* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */