cache/cacheService.js

/**
 * Angular ResourceCacheService
 * Copyright 2016 Andreas Stocker
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
 * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */


(function () {
    'use strict';

    var
        module = angular.module('ngResourceFactory');

    /**
     * Factory that gives a cache class whose instances can be used for caching resources. Instances of this cache class
     * are capable of invalidating related caches, updating resource list instances when getting data from detail calls
     * and respecting the `dataAttr` of resource instances.
     *
     * @name ResourceCacheService
     * @ngdoc service
     * @param {String} name Name of the cache
     * @param {String} pkAttr Name of the primary key attribute on cached instances
     * @param {Object} [options] Additional configuration
     * @param {String|null} options.urlAttr Name of the attribute to get the URL of the objects (default: `null`)
     * @param {String|null} options.dataAttr Name of the attribute to get the actual data from (default: `null`)
     * @param {Boolean} options.wrapObjectsInDataAttr When object cache entries are created from list results, use the `dataAttr` to wrap the data (default: `false`)
     * @param {String[]} options.dependent List of dependent cache names (default: `[]`)
     * @param {int} options.ttl Time to live for cache entries in seconds (default: `3600`)
     * @class
     *
     * @example
     * // Directly using `ResourceCacheService`
     * inject(function (ResourceCacheService) {
     *    var
     *        exampleValue = {pk: 123, attr1: 1, attr2: 2},
     *        cacheInstance = new ResourceCacheService('MyExampleResourceCache', 'pk');
     *
     *    cacheInstance.put('example-1', exampleValue, false);
     *    expect(cacheInstance.get('example-1', false)).toEqual(exampleValue);
     * });
     *
     * @example
     * // Using `ResourceCacheService` for `$http`
     * inject(function (ResourceCacheService, $http) {
     *    var
     *        cacheInstance = new ResourceCacheService('MyExampleResourceCache', 'pk'),
     *        httpInstance = $http({cache: cacheInstance.withoutDataAttr});
     *
     *    httpInstance.get('http://example.com/api/endpoint-1/');
     * });
     */
    module.factory('ResourceCacheService',
        function () {
            'ngInject';

            var
                caches = {};

            function constructor (name, pkAttr, options) {
                var
                    self = this,

                    /**
                     * The cache object
                     * @type {{}}
                     * @private
                     */
                    cache = {},

                    /**
                     * Mapping of cache keys to boolean that indicates whether to use the `dataAttr` or not
                     * @type {{}}
                     * @private
                     */
                    cacheUseDataAttr = {},

                    /**
                     * Mapping of cache keys to boolean that indicates whether the value is managed or not
                     * @type {{}}
                     * @private
                     */
                    cacheIsManaged = {},

                    /**
                     * Mapping of cache keys to timestamps for automatic invalidation
                     * @type {{}}
                     * @private
                     */
                    cacheTimestamps = {};

                options = angular.extend({
                    /**
                     * Name of the attribute to get the URL of the objects
                     * @type {String|null}
                     * @private
                     */
                    urlAttr: null,

                    /**
                     * Name of the attribute to get the actual data from
                     * @type {String|null}
                     * @private
                     */
                    dataAttr: null,

                    /**
                     * When object cache entries are created from list results, use the `dataAttr`
                     * to wrap the data.
                     * @type {Boolean}
                     * @private
                     */
                    wrapObjectsInDataAttr: false,

                    /**
                     * Dependent caches
                     * @type {Array<String>}
                     * @private
                     */
                    dependent: [],

                    /**
                     * Time to live for cache entries in seconds
                     * @type {int}
                     * @private
                     */
                    ttl: 60 * 60
                }, options || {});

                // initialize the cache
                init();

                /**
                 * Gets the first item from cache that matches the given PK value. If there is no item on the
                 * cache that matches, this method returns `undefined`. Note that the cache TTL is ignored.
                 *
                 * @memberOf ResourceCacheService
                 * @function firstByPk
                 * @param {String|int} pkValue PK value to search for
                 * @returns {Object|undefined} Search result data
                 * @instance
                 */
                self.firstByPk = function (pkValue) {
                    for (var key in cacheIsManaged) {
                        if (cacheIsManaged.hasOwnProperty(key) && cacheIsManaged[key]) {
                            var
                                data = getDataForKey(key),
                                isArray = angular.isArray(data),
                                isObject = angular.isObject(data);

                            if (isArray) {
                                for (var i = 0; i < data.length; i++) {
                                    var
                                        dataItem = data[i],
                                        dataItemIsObject = angular.isObject(dataItem);

                                    if (dataItemIsObject && dataItem[pkAttr] == pkValue) {
                                        return dataItem;
                                    }
                                }
                            }
                            else if (isObject && data[pkAttr] == pkValue) {
                                return data;
                            }
                        }
                    }

                    return undefined;
                };

                /**
                 * Gets all items from cache that match the given PK value. If there is no item on the
                 * cache that matches, this method returns an empty array. Note that the cache TTL is ignored.
                 *
                 * @memberOf ResourceCacheService
                 * @function findByPk
                 * @param {String|int} pkValue PK value to search for
                 * @returns {Object[]} Search results data
                 * @instance
                 */
                self.findByPk = function (pkValue) {
                    var
                        result = [];

                    for (var key in cacheIsManaged) {
                        if (cacheIsManaged.hasOwnProperty(key) && cacheIsManaged[key]) {
                            var
                                data = getDataForKey(key),
                                isArray = angular.isArray(data),
                                isObject = angular.isObject(data);

                            if (isArray) {
                                for (var i = 0; i < data.length; i++) {
                                    var
                                        dataItem = data[i],
                                        dataItemIsObject = angular.isObject(dataItem);

                                    if (dataItemIsObject && dataItem[pkAttr] == pkValue) {
                                        result.push(dataItem);
                                    }
                                }
                            }
                            else if (isObject && data[pkAttr] == pkValue) {
                                result.push(data);
                            }
                        }
                    }

                    return result;
                };

                /**
                 * Refreshes the cache entries with the new value or values. The existing objects in the cache
                 * are matched by the `pkAttr` value, and additionally by the `urlAttr`, if available.
                 *
                 * @memberOf ResourceCacheService
                 * @function refresh
                 * @param {Object|Array<Object>} value Instance or list of instances used to refresh data on the cache
                 * @instance
                 */
                self.refresh = function (value) {
                    // refresh the existing values in the cache with the new entries
                    if (angular.isArray(value)) {
                        console.log("ResourceCacheService: Refresh existing entries with list of new entries on the cache '" + name + "'.");

                        refreshEach(value);
                    }
                    // refresh the existing values in the cache with the new entry
                    else if (angular.isObject(value)) {
                        console.log("ResourceCacheService: Refresh existing entries with new entry on the cache '" + name + "'.");

                        refreshSingle(value);
                    }
                    else {
                        console.log("ResourceCacheService: Unable to refresh existing entries on the cache '" + name + "' as given value is neither an array nor an object.");
                    }
                };

                /**
                 * Creates a cache entry for the given value and puts it on the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @function insert
                 * @param {String} key Cache entry key
                 * @param {*} value Cache entry value
                 * @param {Boolean} useDataAttr Use the `dataAttr` to get actual cache entry value
                 * @param {Boolean} [refresh] Refresh the existing cache entries by using the new value
                 * @instance
                 */
                self.insert = function (key, value, useDataAttr, refresh) {
                    console.log("ResourceCacheService: Insert value with key '" + key + "' on the cache '" + name + "'.");

                    var
                        isManaged = angular.isObject(value) || angular.isArray(value),
                        status = 200,
                        headers = isManaged ? {'content-type': 'application/json'} : {},
                        statusText = 'OK',
                        entry = [status, value, headers, statusText];

                    useDataAttr = !!useDataAttr;
                    refresh = angular.isUndefined(refresh) ? true : !!refresh;

                    if (key) {
                        cache[key] = entry;
                        cacheUseDataAttr[key] = useDataAttr && isManaged;
                        cacheIsManaged[key] = isManaged;
                        createOrUpdateTimestamp(key);

                        // only refresh existing data if `refresh` parameter was not set to false
                        if (refresh) {
                            self.refresh(getDataForEntry(entry, useDataAttr));
                        }
                    }
                };

                /**
                 * Puts the given entry with the given key on the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @function put
                 * @param {String} key Cache entry key
                 * @param {*} value Cache entry value
                 * @param {Boolean} useDataAttr Use the `dataAttr` to get actual cache entry value
                 * @instance
                 */
                self.put = function (key, value, useDataAttr) {
                    console.log("ResourceCacheService: Put entry with key '" + key + "' on the cache '" + name + "'.");

                    useDataAttr = !!useDataAttr;

                    var
                        /**
                         * Indicates if value is managed by the cache, which means it is refreshed if new calls
                         * return the same object.
                         * @type {boolean}
                         */
                        isManaged = false;

                    if (key) {
                        // store the actual data object, not the serialized string, for JSON
                        if (value && value[2] && isJsonContentType(value[2]['content-type'])) {
                            console.log("ResourceCacheService: Use deserialized data for key '" + key + "' on the cache '" + name + "'.");

                            value[1] = value[1] ? angular.fromJson(value[1]) : null;
                            isManaged = true;
                        }
                        else {
                            console.log("ResourceCacheService: Use raw data for key '" + key + "' on the cache '" + name + "'.");

                            useDataAttr = false;
                            isManaged = false;
                        }

                        cache[key] = value;
                        cacheUseDataAttr[key] = useDataAttr;
                        cacheIsManaged[key] = isManaged;
                        createOrUpdateTimestamp(key);

                        // only refresh the cache entries if the value is already a cache entry (which is
                        // always an array or object), not a promise.
                        if (isManaged) {
                            self.refresh(getDataForEntry(value, useDataAttr));
                        }
                    }
                };

                /**
                 * Gets the entry with the given key from the cache, or undefined.
                 *
                 * @memberOf ResourceCacheService
                 * @function get
                 * @param {String} key Cache entry key
                 * @param {Boolean} [useCacheTtl] If `true` this method will return `undefined` when the TTL of the entry is outreached (default: `true`)
                 * @returns {*} Cache entry
                 * @instance
                 */
                self.get = function (key, useCacheTtl) {
                    var
                        value = undefined;

                    // `useCacheTtl` should default to true
                    useCacheTtl = angular.isUndefined(useCacheTtl) || !!useCacheTtl ? true : false;

                    if (cache.hasOwnProperty(key)) {
                        if (!useCacheTtl || isEntryAlive(key)) {
                            console.log("ResourceCacheService: Get entry with key '" + key + "' from the cache '" + name + "'.");

                            value = cache[key];

                            // serialize to string for managed objects
                            if (cacheIsManaged[key]) {
                                value = angular.copy(value);
                                value[1] = angular.toJson(value[1]);
                            }
                        }
                        else {
                            console.log("ResourceCacheService: Entry with key '" + key + "' exceeded TTL on the cache '" + name + "'.");

                            self.remove(key);
                        }
                    }
                    else {
                        console.log("ResourceCacheService: Unable to get entry with key '" + key + "' from the cache '" + name + "'.");
                    }

                    return value;
                };

                /**
                 * Removes the entry with the given key from the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @function remove
                 * @param {String} key Cache entry key
                 * @instance
                 */
                self.remove = function (key) {
                    if (cache.hasOwnProperty(key)) {
                        console.log("ResourceCacheService: Remove entry with key '" + key + "' from the cache '" + name + "'.");

                        delete cache[key];
                        delete cacheTimestamps[key];
                        delete cacheUseDataAttr[key];
                        delete cacheIsManaged[key];
                    }
                };

                /**
                 * Removes all entries from the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @function removeAll
                 * @instance
                 */
                self.removeAll = function () {
                    console.log("ResourceCacheService: Remove all entries from the cache '" + name + "'.");

                    for (var key in cache) {
                        if (cache.hasOwnProperty(key)) {
                            delete cache[key];
                            delete cacheTimestamps[key];
                            delete cacheUseDataAttr[key];
                            delete cacheIsManaged[key];
                        }
                    }
                };

                /**
                 * Removes all list entries from the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @function removeAllLists
                 * @instance
                 */
                self.removeAllLists = function () {
                    console.log("ResourceCacheService: Remove all list entries from the cache '" + name + "'.");

                    for (var key in cache) {
                        if (cache.hasOwnProperty(key) && angular.isArray(getDataForKey(key))) {
                            delete cache[key];
                            delete cacheTimestamps[key];
                            delete cacheUseDataAttr[key];
                            delete cacheIsManaged[key];
                        }
                    }
                };

                /**
                 * Removes all list entries from the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @function removeAllObjects
                 * @instance
                 */
                self.removeAllObjects = function () {
                    console.log("ResourceCacheService: Remove all object entries from the cache '" + name + "'.");

                    for (var key in cache) {
                        if (cache.hasOwnProperty(key) && angular.isObject(getDataForKey(key))) {
                            delete cache[key];
                            delete cacheTimestamps[key];
                            delete cacheUseDataAttr[key];
                            delete cacheIsManaged[key];
                        }
                    }
                };

                /**
                 * Removes all raw entries from the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @function removeAllRaw
                 * @instance
                 */
                self.removeAllRaw = function () {
                    console.log("ResourceCacheService: Remove all raw entries from the cache '" + name + "'.");

                    for (var key in cache) {
                        if (cache.hasOwnProperty(key) && !cacheIsManaged[key]) {
                            delete cache[key];
                            delete cacheTimestamps[key];
                            delete cacheUseDataAttr[key];
                            delete cacheIsManaged[key];
                        }
                    }
                };

                /**
                 * Removes all entries of the dependent caches, including the dependent caches of the
                 * dependent caches (and so on ...).
                 *
                 * @memberOf ResourceCacheService
                 * @function removeAllDependent
                 * @instance
                 */
                self.removeAllDependent = function () {
                    var
                        dependentCacheNames = collectDependentCacheNames(self, []);

                    for (var i = 0; i < dependentCacheNames.length; i++) {
                        caches[dependentCacheNames[i]].removeAll();
                    }
                };

                /**
                 * Destroys the cache object.
                 *
                 * @memberOf ResourceCacheService
                 * @function destroy
                 * @instance
                 */
                self.destroy = function () {
                    var
                        cacheIndex = caches.indexOf(self),
                        isManaged = cacheIndex !== -1;

                    if (isManaged) {
                        console.log("ResourceCacheService: Destroy the cache '" + name + "'.");

                        self.removeAll();
                        caches.splice(cacheIndex, 1);
                    }
                };

                /**
                 * Retrieve information regarding the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @function info
                 * @returns {ResourceCacheServiceMeta} Information about the cache instance
                 * @instance
                 */
                self.info = function () {
                    console.log("ResourceCacheService: Get cache information from the cache '" + name + "'.");

                    var
                        size = 0;

                    // calculate the cache size
                    for (var key in cache) {
                        if (cache.hasOwnProperty(key)) {
                            size++;
                        }
                    }

                    return {
                        'id': name,
                        'size': size,
                        'options': options
                    }
                };

                /**
                 * Cache interface to put entries using `dataAttr` on the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @member withDataAttr
                 * @type {HttpCacheInstance}
                 * @instance
                 */
                self.withDataAttr = {
                    put: function (key, value) {
                        return self.put(key, value, true);
                    },
                    get: function (key) {
                        return self.get(key, true);
                    },
                    remove: self.remove,
                    removeAll: self.removeAll,
                    info: self.info
                };

                /**
                 * Cache interface to put entries without using `dataAttr` on the cache.
                 *
                 * @memberOf ResourceCacheService
                 * @member withoutDataAttr
                 * @type {HttpCacheInstance}
                 * @instance
                 */
                self.withoutDataAttr = {
                    put: function (key, value) {
                        return self.put(key, value, false);
                    },
                    get: function (key) {
                        return self.get(key, true);
                    },
                    remove: self.remove,
                    removeAll: self.removeAll,
                    info: self.info
                };

                /**
                 * Cache interface to put entries using `dataAttr` on the cache and ignoring TTL.
                 *
                 * @memberOf ResourceCacheService
                 * @member withDataAttrNoTtl
                 * @type {HttpCacheInstance}
                 * @instance
                 */
                self.withDataAttrNoTtl = {
                    put: function (key, value) {
                        return self.put(key, value, true);
                    },
                    get: function (key) {
                        return self.get(key, false);
                    },
                    remove: self.remove,
                    removeAll: self.removeAll,
                    info: self.info
                };

                /**
                 * Cache interface to put entries without using `dataAttr` on the cache and ignoring TTL.
                 *
                 * @memberOf ResourceCacheService
                 * @member withoutDataAttrNoTtl
                 * @type {HttpCacheInstance}
                 * @instance
                 */
                self.withoutDataAttrNoTtl = {
                    put: function (key, value) {
                        return self.put(key, value, false);
                    },
                    get: function (key) {
                        return self.get(key, false);
                    },
                    remove: self.remove,
                    removeAll: self.removeAll,
                    info: self.info
                };

                /**
                 * Checks if the given content type string indicates JSON.
                 *
                 * @memberOf ResourceCacheService
                 * @function isJsonContentType
                 * @param {String} contentType Content type string to check for
                 * @return {Boolean}
                 * @private
                 */
                function isJsonContentType (contentType) {
                    if (!contentType) {
                        return false;
                    }
                    return (contentType === 'application/json' || String(contentType).indexOf('application/json;') === 0);
                }

                /**
                 * Gets the cache data for the given key.
                 *
                 * @memberOf ResourceCacheService
                 * @function getDataForKey
                 * @param {String} key Cache entry key
                 * @private
                 */
                function getDataForKey (key) {
                    if (cache.hasOwnProperty(key)) {
                        var
                            entry = cache[key],
                            useDataAttr = cacheUseDataAttr[key];

                        return getDataForEntry(entry, useDataAttr);
                    }
                }

                /**
                 * Creates a cache entry from the given data.
                 *
                 * @memberOf ResourceCacheService
                 * @function createEntryForData
                 * @param {Object} data Object to create a cache entry from
                 * @returns {*}
                 * @private
                 */
                function createEntryForData (data) {
                    var
                        entry = {};

                    if (options.wrapObjectsInDataAttr) {
                        entry[options.dataAttr] = data;
                    }
                    else {
                        entry = data;
                    }

                    return entry;
                }

                /**
                 * Gets the cache data for the given cache entry.
                 *
                 * @memberOf ResourceCacheService
                 * @function getDataForEntry
                 * @param {Object} value Object to get the actual value from
                 * @param {Boolean} useDataAttr Use the `dataAttr` to get the actual value from the given object
                 * @returns {*}
                 * @private
                 */
                function getDataForEntry (value, useDataAttr) {
                    var
                        data = value[1];

                    if (useDataAttr && options.dataAttr && data) {
                        return data[options.dataAttr]
                    }
                    else {
                        return data;
                    }
                }

                /**
                 * Sets the cache data for the given key.
                 *
                 * @memberOf ResourceCacheService
                 * @function setDataForKey
                 * @param {String} key Cache entry key
                 * @param {*} newData New value for the cache entry
                 * @private
                 */
                function setDataForKey (key, newData) {
                    if (cache.hasOwnProperty(key)) {
                        var
                            entry = cache[key],
                            entryUseDataAttr = cacheUseDataAttr[key],
                            entryData = entry[1];

                        if (entryUseDataAttr && options.dataAttr && entryData) {
                            entryData[options.dataAttr] = newData;
                        }
                        else {
                            entryData = newData;
                        }

                        entry[1] = entryData;
                    }
                }

                /**
                 * Returns the current unix epoch in seconds.
                 *
                 * @memberOf ResourceCacheService
                 * @function getCurrentTimestamp
                 * @returns {int} Current unix epoch
                 * @private
                 */
                function getCurrentTimestamp () {
                    return Math.floor(Date.now() / 1000);
                }

                /**
                 * Sets the timestamp for the given key to the current unix epoch in seconds.
                 *
                 * @memberOf ResourceCacheService
                 * @function createOrUpdateTimestamp
                 * @param {String} key Cache entry key
                 * @returns {int} Current unix epoch
                 * @private
                 */
                function createOrUpdateTimestamp (key) {
                    cacheTimestamps[key] = getCurrentTimestamp();
                    return cacheTimestamps[key];
                }

                /**
                 * Checks if the cache entry for the given key is still alive. Also returns
                 * `false` if there is no cache entry for the given key.
                 *
                 * @memberOf ResourceCacheService
                 * @function isEntryAlive
                 * @param {String} key Cache entry key
                 * @returns {Boolean} Cache entry alive or not alive
                 * @private
                 */
                function isEntryAlive (key) {
                    if (cache.hasOwnProperty(key)) {
                        var
                            entryAge = getCurrentTimestamp() - cacheTimestamps[key];

                        return entryAge <= options.ttl;
                    }

                    return false;
                }

                /**
                 * Takes a new cache entry and refreshes the existing instances of the entry, matching by the
                 * `pkAttr` value.
                 *
                 * @memberOf ResourceCacheService
                 * @function refreshSingle
                 * @param {Object} newData New data to refresh the cache with
                 * @private
                 */
                function refreshSingle (newData) {
                    var
                        urlAttr = options.urlAttr;

                    // inserts the data on the cache as individual entry, if we have the URL information on the data
                    if (urlAttr && newData && newData[urlAttr]) {
                        self.insert(newData[urlAttr], createEntryForData(newData), options.wrapObjectsInDataAttr, false);
                    }

                    for (var key in cache) {
                        if (cache.hasOwnProperty(key) && cacheIsManaged[key]) {
                            var
                                entry = cache[key],
                                entryUseDataAttr = cacheUseDataAttr[key],
                                entryData = getDataForEntry(entry, entryUseDataAttr),
                                isList = angular.isArray(entryData);

                            // refresh the objects matching the new object within the list entries in the cache
                            if (isList) {
                                for (var i = 0; i < entryData.length; i++) {
                                    if (entryData[i][pkAttr] === newData[pkAttr]) {
                                        // additionally compare the `urlAttr`, if available
                                        if (!urlAttr || (urlAttr && entryData[i][urlAttr] === newData[urlAttr])) {
                                            entryData[i] = newData;
                                        }
                                    }
                                }

                                // update the cache entry with the new data
                                setDataForKey(key, entryData);
                            }
                            // refresh the objects matching the new object in the cache
                            else {
                                if (entryData[pkAttr] === newData[pkAttr]) {
                                    // additionally compare the `urlAttr`, if available
                                    if (!urlAttr || (urlAttr && entryData[urlAttr] === newData[urlAttr])) {
                                        setDataForKey(key, newData);

                                        // for object entries we can update the entries timestamp
                                        createOrUpdateTimestamp(key);
                                    }
                                }
                            }
                        }
                    }
                }

                /**
                 * Refreshes each entry in the given list using the `refreshSingle` method.
                 *
                 * @memberOf ResourceCacheService
                 * @function refreshEach
                 * @param {Array<Object>} newEntries List of new data to refresh the cache with
                 * @private
                 */
                function refreshEach (newEntries) {
                    for (var i = 0; i < newEntries.length; i++) {
                        if (angular.isObject(newEntries[i])) {
                            refreshSingle(newEntries[i]);
                        }
                    }
                }

                /**
                 * Initializes the cache object.
                 *
                 * @memberOf ResourceCacheService
                 * @function init
                 * @private
                 */
                function init () {
                    // make sure the given name is not used yet
                    if (caches.hasOwnProperty(name)) {
                        throw Error("Name '" + name + "' is already used by another cache.");
                    }

                    caches[name] = self;
                }
            }

            /**
             * Calls the removeAll method on all managed caches.
             *
             * @memberOf ResourceCacheService
             * @function removeAll
             * @static
             */
            constructor.removeAll = function () {
                for (var key in caches) {
                    if (caches.hasOwnProperty(key)) {
                        caches[key].removeAll();
                    }
                }
            };

            /**
             * Gets the cache with the given name, or null.
             *
             * @memberOf ResourceCacheService
             * @function get
             * @param {String} key Cache name
             * @returns {ResourceCacheService} Cache instance with the given name
             * @static
             */
            constructor.get = function (key) {
                if (caches.hasOwnProperty(key)) {
                    return caches[key];
                }

                console.log("ResourceCacheService: Cache '" + key + "' does not exist.");

                return null;
            };

            /**
             * Gets the cache information for all managed caches as mapping of cacheId to the result
             * of the info method on the cache.
             *
             * @memberOf ResourceCacheService
             * @function info
             * @returns {Object} Information object for all caches
             * @static
             */
            constructor.info = function () {
                var
                    infos = {};

                for (var key in caches) {
                    if (caches.hasOwnProperty(key)) {
                        var
                            info = caches[key].info();

                        infos[info.id] = info;
                    }
                }

                return infos;
            };

            /**
             * Collects all dependent caches of the given cache, including the dependent caches of the dependent
             * caches (and so on ...).
             *
             * @memberOf ResourceCacheService
             * @function collectDependentCacheNames
             * @param {Object} cache
             * @param {Array<String>|undefined} collectedDependentCacheNames
             * @returns {Array<String>}
             * @private
             */
            function collectDependentCacheNames (cache, collectedDependentCacheNames) {
                var
                    cacheDependentCacheNames = cache.info()['options']['dependent'];

                // default `collectedDependentCacheNames` to empty list
                collectedDependentCacheNames = collectedDependentCacheNames || [];

                for (var i = 0; i < cacheDependentCacheNames.length; i++) {
                    var
                        cacheDependentCacheName = cacheDependentCacheNames[i],
                        cacheDependentCache = caches[cacheDependentCacheName];

                    if (cacheDependentCache) {
                        // push cache name to the collected dependent caches, if existing
                        collectedDependentCacheNames.push(cacheDependentCacheName);

                        // only collect cache dependencies if not already collected, to prevent circles
                        if (collectedDependentCacheNames.indexOf(cacheDependentCacheName) === -1) {
                            collectDependentCacheNames(cacheDependentCache, collectedDependentCacheNames)
                        }
                    }
                }

                return collectedDependentCacheNames;
            }

            return constructor;
        }
    );
})();