# Source: data/service/data-service.js

    var Montage = require("core/core").Montage,
        AuthorizationManager = require("data/service/authorization-manager").AuthorizationManager,
        AuthorizationPolicy = require("data/service/authorization-policy").AuthorizationPolicy,
        DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor,
        DataQuery = require("data/model/data-query").DataQuery,
        DataStream = require("data/service/data-stream").DataStream,
        DataTrigger = require("data/service/data-trigger").DataTrigger,
        Map = require("collections/map"),
        Promise = require("core/promise").Promise,
        ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor,
        Set = require("collections/set"),
        WeakMap = require("collections/weak-map");

    var AuthorizationPolicyType = new Montage();
    AuthorizationPolicyType.NoAuthorizationPolicy = AuthorizationPolicy.NONE;
    AuthorizationPolicyType.UpfrontAuthorizationPolicy = AuthorizationPolicy.UP_FRONT;
    AuthorizationPolicyType.OnDemandAuthorizationPolicy = AuthorizationPolicy.ON_DEMAND;
    AuthorizationPolicyType.OnFirstFetchAuthorizationPolicy = AuthorizationPolicy.ON_FIRST_FETCH;

    /**
     * Provides data objects and manages changes to them.
     *
     * Data service subclasses that implement their own constructor should call this
     * class' constructor at the beginning of their constructor implementation
     * with code like the following:
     *
     *     DataService.call(this);
     *
     * Currently only one service tree with one
     * [root services]{@link DataService#rootService} is supported, and every
     * instance of DataService or a DataService subclasses must either be that root
     * service or be set as a descendent of that root service.
     *
     * @class
     * @extends external:Montage
     */
    exports.DataService = Montage.specialize(/** @lends DataService.prototype */ {

        /***************************************************************************
         * Initializing
         */

        constructor: {
            value: function DataService() {
                exports.DataService.mainService = exports.DataService.mainService || this;
                this._initializeAuthorization();
                this._initializeOffline();
            }
        },

        /***************************************************************************
         * Serialization
         */

        deserializeSelf: {
            value:function (deserializer) {
                var value;
                value = deserializer.getProperty("childServices");
                if (value) {
                    this.registerChildServices(value);
                }

                value = deserializer.getProperty("model") || deserializer.getProperty("binder");
                if (value) {
                    this.model = value;
                }

                value = !this.model && deserializer.getProperty("types");
                if (value) {
                    Array.prototype.push.apply(this._childServiceTypes, value);
                }

                value = deserializer.getProperty("mappings");
                if (value) {
                    Array.prototype.push.apply(this._childServiceMappings, value);
                }

                value = deserializer.getProperty("delegate");
                if (value) {
                    this.delegate = value;
                }

            }
        },

        delegate: {
            value: null
        },

        /***************************************************************************
         * Basic Properties
         *
         * Private properties are defined where they are used, not here.
         */

        /**
         * The types of data handled by this service. If this `undefined`, `null`,
         * or an empty array this service is assumed to handled all types of data.
         *
         * The default implementation of this property returns the union of all
         * types handled by child services of this service. Subclasses without child
         * services should override this to directly return an array of the specific
         * types they handle.
         *
         * Applications typically have one [raw data service]{@link RawDataService}
         * service for each set of related data types and one
         * [main service]{@link DataService.mainService} which is the parent of all
         * those other services and delegates work to them based on the type of data
         * to which the work applies. It is possible for child data services to have
         * children of their own and delegate some or all of their work to them.
         *
         * A service's types must not be changed after it is added as a child of
         * another service.
         *
         * @type {Array.<DataObjectDescriptor>}
         */
        types: {
            get: function () {
                return this._childServiceTypes;
            }
        },

        /**
         * The data mappings used by this service to convert objects to raw
         * data and vice-versa.
         *
         * @type {Array.<DataMapping>}
         */
        mappings: {
            get: function () {
                return this._childServiceMappings;
            }
        },

        /***************************************************************************
         * Service Hierarchy
         */

        /**
         * A read-only reference to the parent of this service.
         *
         * This value is modified by calls to
         * [addChildService()]{@link DataService#addChildService} and
         * [removeChildService()]{@link DataService#removeChildService} and cannot
         * be modified directly.
         *
         * Data services that have no parents are called
         * [root services]{@link DataService#rootService}.
         *
         * @type {?DataService}
         */
        parentService: {
            get: function () {
                return this._parentService;
            }
        },

        /**
         * Private settable parent service reference.
         *
         * This property's value should not be modified outside of
         * [addChildService()]{@link DataService#addChildService} and
         * [removeChildService()]{@link DataService#removeChildService}.
         *
         * @private
         * @type {?DataService}
         */
        _parentService: {
            value: undefined
        },

        /**
         * Convenience read-only reference to the root of the service tree
         * containing this service. Most applications have only one root service,
         * the application's [main service]{@link DataService.mainService}.
         *
         * @type {DataService}
         */
        rootService: {
            get: function () {
                return this.parentService ? this.parentService.rootService : this;
            }
        },

        /**
         * Convenience method to assess if a dataService is the rootService
         *
         * @type {Boolean}
         */
        isRootService: {
            get: function () {
                return this === this.rootService;
            }
        },

        /**
         * The child services of this service.
         *
         * This value is modified by calls to
         * [addChildService()]{@link DataService#addChildService} and
         * [removeChildService()]{@link DataService#removeChildService} and must not
         * be modified directly.
         *
         * @type {Set.<DataService>}
         */
        childServices: {
            get: function() {
                if (!this._childServices) {
                    this._childServices = new Set();
                }
                return this._childServices;
            }
        },

        /**
         * Private settable child service set.
         *
         * This property should not be modified outside of the
         * [childServices getter]{@link DataService#childServices}, and its contents
         * should not be modified outside of
         * [addChildService()]{@link DataService#addChildService} and
         * [removeChildService()]{@link DataService#removeChildService}
         *
         * @private
         * @type {?Set.<DataService>}
         */
        _childServices: {
            value: undefined
        },

        /**
         * Adds a raw data service as a child of this data service and set it to
         * handle data of the types defined by its [types]{@link DataService#types}
         * property.
         *
         * Child services must have their [types]{@link DataService#types} property
         * value or their [model]{@link DataService#model} set before they are passed in to
         * this method, and that value cannot change after that.  The model property takes
         * priority of the types property.  If the model is defined the service will handle
         * all the object descriptors associated to the model.
         *
         * @method
         * @argument {RawDataService} service
         * @argument {Array} [types] Types to use instead of the child's types.
         */
        addChildService: {
            value: function (child, types) {
                if (child instanceof exports.DataService &&
                    child.constructor !== exports.DataService) {
                    this._addChildService(child, types);
                } else {
                    console.warn("Cannot add child -", child);
                    console.warn("Children must be instances of DataService subclasses.");
                }
            }
        },

        _addChildService: {
            value: function (child, types) {
                var children, type, i, n, nIfEmpty = 1;
                types = types || child.model && child.model.objectDescriptors || child.types;
                // If the new child service already has a parent, remove it from
                // that parent.
                if (child._parentService) {
                    child._parentService.removeChildService(child);
                }

                // Add the new child to this service's children set.
                this.childServices.add(child);
                this._childServicesByIdentifier.set(child.identifier, child);
                // Add the new child service to the services array of each of its
                // types or to the "all types" service array identified by the
                // `null` type, and add each of the new child's types to the array
                // of child types if they're not already there.

                for (i = 0, n = types && types.length || nIfEmpty; i < n; i += 1) {
                    type = types && types.length && types[i] || null;
                    children = this._childServicesByType.get(type) || [];
                    children.push(child);
                    if (children.length === 1) {
                        this._childServicesByType.set(type, children);
                        if (type) {
                            this._childServiceTypes.push(type);
                        }
                    }
                }
                // Set the new child service's parent.
                child._parentService = this;
            }
        },

        __childServiceRegistrationPromise: {
            value: null
        },

        _childServiceRegistrationPromise: {
            get: function() {
                return this.__childServiceRegistrationPromise || (this.__childServiceRegistrationPromise = Promise.resolve());
            },
            set: function(value) {
                this.__childServiceRegistrationPromise = value;
            }
        },

        registerChildServices: {
            value: function (childServices) {
                var self;
                if (!this.__childServiceRegistrationPromise) {
                    self = this;
                    this.__childServiceRegistrationPromise = Promise.all(childServices.map(function (child) {
                        return self.registerChildService(child);
                    }));
                }
            }
        },

        /**
         * Alternative to [addChildService()]{@link DataService#addChildService}.
         * While addChildService is synchronous, registerChildService is asynchronous
         * and may take a child whose [types]{@link DataService#types} property is
         * a promise instead of an array.
         *
         * This is useful for example if the child service does not know its types
         * immediately, e.g. if it must fetch them from a .mjson descriptors file.
         *
         * If the child's types is an array, it is guaranteed to behave exactly
         * like addChildService.
         *
         * @method
         * @param {DataService} child service to add to this service.
         * @param {?Promise|ObjectDescriptor|Array<ObjectDescriptor>}
         * @return {Promise}
         */
        registerChildService: {
            value: function (child, types) {
                var self = this,
                    mappings = child.mappings || [];
                // possible types
                // -- types is passed in as an array or a single type.
                // -- a model is set on the child.
                // -- types is set on the child.
                // any type can be asychronous or synchronous.
                    types = types && Array.isArray(types) && types ||
                            types && [types] ||
                            child.model && child.model.objectDescriptors ||
                            child.types && Array.isArray(child.types) && child.types ||
                            child.types && [child.types] ||
                            [];

                return child._childServiceRegistrationPromise.then(function () {
                    return self._registerChildServiceTypesAndMappings(child, types, mappings);
                });
            }
        },

        // #1 resolve asynchronous types
        // -- types are arrays
        // -- contents of the array can be:
        // ---- an objectDescriptor or
        // ---- a promise for an objectDescriptor or
        // ---- a promise for an array of objectDescriptors
        // -- flatten the result
        // #2 map module id to object descriptor
        // #3 register mapping to objectDescriptor
        // -- resolve the mappings references
        // -- map objectDescriptor to mapping
        // #4 make prototype for object descriptor
        // -- map constructor to prototype
        // -- map objectDescriptor to prototype
        // -- map objectDescriptor to dataTriggers

        // -- TODO: dataTriggers should be derived from all properties - mapping requisitePropertyNames

        _registerChildServiceTypesAndMappings: {
            value: function (child, types, mappings) {
                var self = this,
                    objectDescriptors;
                return this._resolveAsynchronousTypes(types).then(function (descriptors) {
                    objectDescriptors = descriptors;
                    self._registerTypesByModuleId(objectDescriptors);
                    return self._registerChildServiceMappings(child, mappings);
                }).then(function () {
                    return self._makePrototypesForTypes(objectDescriptors);
                }).then(function () {
                    self.addChildService(child, types);
                    return null;
                });
            }
        },

        _resolveAsynchronousTypes: {
            value: function (types) {
                var self = this;
                return Promise.all(this._flattenArray(types).map(function (type) {
                    return type instanceof Promise ? type : Promise.resolve(type);
                })).then(function (descriptors) {
                    return self._flattenArray(descriptors);
                });
            }
        },

        _flattenArray: {
            value: function (array) {
                return Array.prototype.concat.apply([], array);
            }
        },

        _registerTypesByModuleId: {
            value: function (types) {
                var map = this._moduleIdToObjectDescriptorMap;
                types.forEach(function (objectDescriptor) {
                    var module = objectDescriptor.module,
                        moduleId = [module.id, objectDescriptor.exportName].join("/");
                    map[moduleId] = objectDescriptor;
                });
            }
        },

        _registerChildServiceMappings: {
            value: function (child, mappings) {
                var self = this;
                return Promise.all(mappings.map(function (mapping) {
                    return self._addMappingToChild(mapping, child);
                }));
            }
        },

        _makePrototypesForTypes: {
            value: function (types) {
                var self = this;
                return Promise.all(types.map(function (objectDescriptor) {
                    return self._makePrototypeForType(objectDescriptor);
                }));
            }
        },

        _makePrototypeForType: {
            value: function (objectDescriptor) {
                var self = this,
                    module = objectDescriptor.module;
                return module.require.async(module.id).then(function (exports) {
                    var constructor = exports[objectDescriptor.exportName],
                        prototype = Object.create(constructor.prototype),
                        mapping = self.mappingWithType(objectDescriptor),
                        requisitePropertyNames = mapping && mapping.requisitePropertyNames || new Set(),
                        dataTriggers = DataTrigger.addTriggers(self, objectDescriptor, prototype, requisitePropertyNames);
                    self._dataObjectPrototypes.set(constructor, prototype);
                    self._dataObjectPrototypes.set(objectDescriptor, prototype);
                    self._dataObjectTriggers.set(objectDescriptor, dataTriggers);
                    self._constructorToObjectDescriptorMap.set(constructor, objectDescriptor);
                    return null;
                });
            }
        },

        _addMappingToChild: {
            value: function (mapping, child) {
                var service = this;
                return Promise.all([
                    mapping.objectDescriptorReference,
                    mapping.schemaDescriptorReference
                ]).spread(function (objectDescriptor, schemaDescriptor) {
                    // TODO -- remove looking up by string to unique.
                    var type = [objectDescriptor.module.id, objectDescriptor.name].join("/");
                    objectDescriptor = service._moduleIdToObjectDescriptorMap[type];
                    mapping.objectDescriptor = objectDescriptor;
                    mapping.schemaDescriptor = schemaDescriptor;
                    mapping.service = child;
                    child.addMappingForType(mapping, objectDescriptor);
                    return null;
                });
            }
        },

        _objectDescriptorForType: {
            value: function (type) {
                return  this._constructorToObjectDescriptorMap.get(type) ||
                        typeof type === "string" && this._moduleIdToObjectDescriptorMap[type] ||
                        type;
            }
        },

        _constructorToObjectDescriptorMap: {
            get: function () {
                if (!this.__constructorToObjectDescriptorMap) {
                    this.__constructorToObjectDescriptorMap = new Map();
                }
                return this.__constructorToObjectDescriptorMap;
            }
        },

        _moduleIdToObjectDescriptorMap: {
            get: function () {
                if (!this.__moduleIdToObjectDescriptorMap) {
                    this.__moduleIdToObjectDescriptorMap = {};
                }
                return this.__moduleIdToObjectDescriptorMap;
            }
        },

        /**
         * Remove a raw data service as a child of this service and clear its parent
         * if that service is a child of this service.
         *
         * The performance of this method is O(m) + O(n), where m is the number of
         * children of this service handling the same type as the child service to
         * remove and n is the number of types handled by all children of this
         * service.
         *
         * @method
         * @argument {RawDataService} service
         * @argument {Array} [types] Types to use instead of the child's types.
         */
        removeChildService: {
            value: function (child, types) {
                var type, chidren, index, i, n;
                types = types || child.types;
                // Remove the child service from the services array of each of its
                // types or from the "all types" service array identified by the
                // `null` type, or remove a type altogether if its service array
                // only contains the child service to remove, or remove the "all
                // types" service array if it only contains the child service to
                // remove.
                for (i = 0, n = types && types.length || 1; i < n; i += 1) {
                    type = types && types.length && types[i] || null;
                    chidren = this._childServicesByType.get(type);
                    index = chidren ? chidren.indexOf(child) : -1;
                    if (index >= 0 && chidren.length > 1) {
                        chidren.splice(index, 1);
                    } else if (index === 0) {
                        this._childServicesByType.delete(type);
                        index = type ? this._childServiceTypes.indexOf(type) : -1;
                        if (index >= 0) {
                            this._childServiceTypes.splice(index, 1);
                        }
                    }
                }
                // Remove the child from this service's children set.
                this.childServices.delete(child);
                // Clear the service parent if appropriate.
                if (child._parentService === this) {
                    child._parentService = undefined;
                }
            }
        },

        /**
         * Alternative to [removeChildService()]{@link DataService#removeChildService}.
         * While removeChildService is synchronous, unregisterChildService is asynchronous
         * and may take a child whose [types]{@link DataService#types} property is
         * a promise instead of an array.
         *
         * This is useful for example if the child service does not know its types
         * immediately, e.g. if it must fetch them from a .mjson descriptors file.
         *
         * If the child's types is an array, it is guaranteed to behave exactly
         * like removeChildService.
         *
         * @method
         * @return {Promise}
         */
        unregisterChildService: {
            value: function (child) {
                var self = this;
                return new Promise(function (resolve, reject) {
                    self.removeChildService(child, child.types);
                    resolve();
                });
            }
        },

        /**
         * A map from each of the data types handled by this service to an array
         * of the child services that can handle that type, with each such array
         * ordered according to the order in which the services in it were
         * [added]{@link DataService#addChildService} as children of this service.
         *
         * If one or more child services of this service are defined as handling all
         * types (their [types]{@link DataService#types} property is `undefined`,
         * `null`, or an empty array), the child service map also include a `null`
         * key whose corresponding value is an array of all those services defined
         * to handle all types.
         *
         * The contents of this map should not be modified outside of
         * [addChildService()]{@link DataService#addChildService} and
         * [removeChildService()]{@link DataService#removeChildService}.
         *
         * @private
         * @type {Map<DataObjectDescriptor, Array.<DataService>>}
         */
        _childServicesByType: {
            get: function () {
                if (!this.__childServicesByType) {
                    this.__childServicesByType = new Map();
                }
                return this.__childServicesByType;
            }
        },

        __childServicesByType: {
            value: undefined
        },

        _childServicesByIdentifier: {
            get: function () {
                if (!this.__childServicesByIdentifier) {
                    this.__childServicesByIdentifier = new Map();
                }
                return this.__childServicesByIdentifier;
            }
        },

        __childServicesByIdentifier: {
            value: undefined
        },

        /**
         * An array of the data types handled by all child services of this service.
         *
         * The contents of this map should not be modified outside of
         * [addChildService()]{@link DataService#addChildService} and
         * [removeChildService()]{@link DataService#removeChildService}.
         *
         * @private
         * @type {Array.<DataObjectDescriptor>}
         */
        _childServiceTypes: {
            get: function() {
                if (!this.__childServiceTypes) {
                    this.__childServiceTypes = [];
                }
                return this.__childServiceTypes;
            }
        },

        __childServiceTypes: {
            value: undefined
        },

        /**
         * Get the first child service that can handle the specified object,
         * or `null` if no such child service exists.
         *
         * @private
         * @method
         * @argument {Object} object
         * @returns DataService
         */
        _getChildServiceForObject: {
            value: function (object) {
                return this.childServiceForType(this.rootService._getObjectType(object));
            }
        },

        /**
         * Get the first child service that can handle data of the specified type,
         * or `null` if no such child service exists.
         *
         * @private
         * @method
         * @argument {DataObjectDescriptor} type
         * @returns {Set.<DataService,number>}
         */
        childServiceForType: {
            value: function (type) {
                var services;
                type = type instanceof ObjectDescriptor ? type : this._objectDescriptorForType(type);
                services = this._childServicesByType.get(type) || this._childServicesByType.get(null);
                return services && services[0] || null;
            }
        },

        /***************************************************************************
         * Mappings
         */

        /**
         * Adds a mapping to the service for the specified
         * type.
         * @param {DataMapping} mapping.  The mapping to use.
         * @param {ObjectDescriptor} type.  The object type.
         */
        addMappingForType: {
            value: function (mapping, type) {
                this._mappingByType.set(type, mapping);
            }
        },

        /**
         * Return the mapping to use for the specified type.
         * @param {ObjectDescriptor} type.
         * @returns {DataMapping|null} returns the specified mapping or null
         * if a mapping is not defined for the specified type.
         */
        mappingWithType: {
            value: function (type) {
                var mapping;
                type = this._objectDescriptorForType(type);
                mapping = this._mappingByType.has(type) && this._mappingByType.get(type);
                return mapping || null;
            }
        },

        _mappingByType: {
            get: function () {
                if (!this.__mappingByType) {
                    this.__mappingByType = new Map();
                }
                return this.__mappingByType;
            }
        },

        __mappingByType: {
            value: undefined
        },

        _childServiceMappings: {
            get: function () {
                if (!this.__childServiceMappings) {
                    this.__childServiceMappings = [];
                }
                return this.__childServiceMappings;
            }
        },

        __childServiceMappings: {
            value: undefined
        },

        /***************************************************************************
         * Models
         */

        /**
         * The [model]{@link ObjectModel} that this service supports.  If the model is
         * defined the service supports all the object descriptors contained within the model.
         */
        model: {
            value: undefined
        },

        /**
         * The maximum amount of time a DataService's data will be considered fresh.
         * ObjectDescriptor's maxAge should take precedence over this and a DataStream's dataMaxAge should
         * take precedence over a DataService's dataMaxAge global default value.
         *
         * @type {Number}
         */
        dataMaxAge: {
            value: undefined
        },

        /***************************************************************************
         * Authorization
         */

        _initializeAuthorization: {
            value: function () {
                if (this.providesAuthorization) {
                    exports.DataService.authorizationManager.registerAuthorizationService(this);
                }

                if (this.authorizationPolicy === AuthorizationPolicyType.UpfrontAuthorizationPolicy) {
                    var self = this;
                    exports.DataService.authorizationManager.authorizeService(this).then(function(authorization) {
                        self.authorization = authorization;
                        return authorization;
                    }).catch(function(error) {
                        console.log(error);
                    });
                } else {
                    //Service doesn't need anything upfront, so we just go through
                    this.authorizationPromise = Promise.resolve();
                }
            }
        },

        /**
         * Returns the AuthorizationPolicyType used by this DataService.
         *
         * @type {AuthorizationPolicyType}
         */
        authorizationPolicy: {
            value: AuthorizationPolicyType.NoAuthorizationPolicy
        },

        /**
         * holds authorization object after a successfull authorization
         *
         * @type {Object}
         */

        authorization: {
            value: undefined
        },

        authorizationPromise: {
            value: Promise.resolve()
        },

        /**
         * Returns the list of moduleIds of DataServices a service accepts to provide
         * authorization on its behalf. If an array has multiple
         * authorizationServices, the final choice will be up to the App user
         * regarding which one to use. This array is expected to return moduleIds,
         * not objects, allowing the AuthorizationManager to manage unicity
         *
         * @type {string[]}
         */
        authorizationServices: {
            value: null
        },

        /**
         * @type {string}
         * @description Module ID of the panel component used to gather necessary authorization information
         */
        authorizationPanel: {
            value: undefined
        },

        /**
         * Indicates whether a service can provide user-level authorization to its
         * data. Defaults to false. Concrete services need to override this as
         * needed.
         *
         * @type {boolean}
         */
        providesAuthorization: {
            value: false
        },

        /**
         *
         *
         * @method
         * @returns Promise
         */
        authorize: {
            value: function () {
                console.warn("DataService.authorize() must be overridden by the implementing service", arguments);
                return this.nullPromise;
            }
        },
        /**
         *
         * @method
         * @returns Promise
         */
        logOut: {
            value: function () {
                console.warn("DataService.logOut() must be overridden by the implementing service");
                return this.nullPromise;
            }
        },

        /***************************************************************************
         * Data Object Types
         */

        /**
         * Returns an object descriptor for the provided object.  If this service
         * does not have an object descriptor for this object it will ask its
         * parent for one.
         * @param {object}
         * @returns {ObjectDescriptor|null} if an object descriptor is not found this
         * method will return null.
         */
        objectDescriptorForObject: {
            value: function (object) {
                var types = this.types,
                    objectInfo = Montage.getInfoForObject(object),
                    moduleId = objectInfo.moduleId,
                    objectName = objectInfo.objectName,
                    module, exportName, objectDescriptor, i, n;
                for (i = 0, n = types.length; i < n && !objectDescriptor; i += 1) {
                    module = types[i].module;
                    exportName = module && types[i].exportName;
                    if (module && moduleId === module.id && objectName === exportName) {
                        objectDescriptor = types[i];
                    }
                }
                return objectDescriptor || this.parentService && this.parentService.objectDescriptorForObject(object);
            }
        },

        /**
         * Get the type of the specified data object.
         *
         * @private
         * @method
         * @argument {Object} object       - The object whose type is sought.
         * @returns {DataObjectDescriptor} - The type of the object, or undefined if
         * no type can be determined.
         */
        _getObjectType: {
            value: function (object) {
                var type = this._typeRegistry.get(object),
                    moduleId = typeof object === "string" ? object : this._getModuleIdForObject(object);
                while (!type && object) {
                    if (object.constructor.TYPE instanceof DataObjectDescriptor) {
                        type = object.constructor.TYPE;
                    } else if (this._moduleIdToObjectDescriptorMap[moduleId]) {
                        type = this._moduleIdToObjectDescriptorMap[moduleId];
                    } else {
                        object = Object.getPrototypeOf(object);
                    }
                }
                return type;
            }
        },

        _getModuleIdForObject: {
            value: function (object) {
                var info = Montage.getInfoForObject(object);
                return [info.moduleId, info.objectName].join("/");
            }
        },

        /**
         * Register the type of the specified data object if necessary.
         *
         * @private
         * @method
         * @argument {Object} object
         * @argument {DataObjectDescriptor} type
         */
        _setObjectType: {
            value: function (object, type) {
                if (this._getObjectType(object) !== type){
                    this._typeRegistry.set(object, type);
                }
            }
        },

        _typeRegistry: {
            get: function () {
                if (!this.__typeRegistry){
                    this.__typeRegistry = new WeakMap();
                }
                return this.__typeRegistry;
            }
        },

        /***************************************************************************
         * Data Object Triggers
         */

        /**
         * Returns a prototype for objects of the specified type. The returned
         * prototype will have a [data trigger]{@link DataTrigger} defined for each
         * lazy relationships and properties of that type. A single prototype will
         * be created for all objects of a given type.
         *
         * @private
         * @method
         * @argument {DataObjectDescriptor|ObjectDescriptor} type
         * @returns {Object}
         */
        _getPrototypeForType: {
            value: function (type) {
                var prototype;
                type = this._objectDescriptorForType(type);
                prototype = this._dataObjectPrototypes.get(type);
                if (type && !prototype) {
                    prototype = Object.create(type.objectPrototype || Montage.prototype);
                    this._dataObjectPrototypes.set(type, prototype);
                    this._dataObjectTriggers.set(type, DataTrigger.addTriggers(this, type, prototype));

                    //We add a property that returns an object's snapshot
                    //We add a property that returns an object's primaryKey
                    //Let's postponed this for now and revisit when we need
                    //add more properties/logic to automatically track changes
                    //on objects

                    // Object.defineProperties(prototype, {
                    //      "montageDataSnapshot": {
                    //          get: this.__object__snapshotMethodImplementation
                    //      },
                    //      "montageDataPrimaryKey": {
                    //          get: this.__object_primaryKeyMethodImplementation
                    //      }
                    //  });

                }
                return prototype;
            }
        },

        // __object__snapshotMethodImplementation: {
        //     value: function() {
        //         debugger;
        //         return exports.DataService.mainService._getChildServiceForObject(this).snapshotForObject(this);
        //     }
        // },
        // __object_primaryKeyMethodImplementation: {
        //     value: function() {
        //         debugger;
        //         return exports.DataService.mainService.dataIdentifierForObject(this).primaryKey;
        //     }
        // },

        /**
         * Returns the [data triggers]{@link DataTrigger} set up for objects of the
         * specified type.
         *
         * @private
         * @method
         * @argument {Object} object
         * @returns {Object<string, DataTrigger>}
         */
        _getTriggersForObject: {
            value: function (object) {
                var type = this._getObjectType(object);
                return type && this._dataObjectTriggers.get(type);
            }
        },

        _dataObjectPrototypes: {
            get: function () {
                if (!this.__dataObjectPrototypes){
                    this.__dataObjectPrototypes = new Map();
                }
                return this.__dataObjectPrototypes;
            }
        },

        __dataObjectPrototypes: {
            value: undefined
        },

        _dataObjectTriggers: {
            get: function () {
                if (!this.__dataObjectTriggers){
                    this.__dataObjectTriggers = new Map();
                }
                return this.__dataObjectTriggers;
            }
        },

        __dataObjectTriggers: {
            value: undefined
        },

        /***************************************************************************
         * Data Object Properties
         */

        /**
         * Since root services are responsible for triggering data objects fetches,
         * subclasses whose instances will not be root services should override this
         * method to call their root service's implementation of it.
         *
         * @todo Rename and document API and implementation.
         *
         * @method
         */
        decacheObjectProperties: {
            value: function (object, propertyNames) {
                if (this.isRootService) {
                    var names = Array.isArray(propertyNames) ? propertyNames : arguments,
                        start = names === propertyNames ? 0 : 1,
                        triggers = this._getTriggersForObject(object),
                        trigger, i, n;
                    for (i = start, n = names.length; i < n; i += 1) {
                        trigger = triggers && triggers[names[i]];
                        if (trigger) {
                            trigger.decacheObjectProperty(object);
                        }
                    }
                }
                else {
                    this.rootService.decacheObjectProperties(object, propertyNames);
                }

            }
        },

        /**
         * Request possibly asynchronous values of a data object's properties. These
         * values will only be fetched if necessary and only the first time they are
         * requested.
         *
         * To force an update of a value that was previously obtained or set, use
         * [updateObjectProperties()]{@link DataService#updateObjectProperties}
         * instead of this method.
         *
         * Since root services are responsible for determining when to fetch or
         * update data objects values, subclasses whose instances will not be root
         * services should override this method to call their root service's
         * implementation of it.
         *
         * Subclasses should define how property values are obtained by overriding
         * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead
         * of this method. That method will be called by this method when needed.
         *
         * Although this method returns a promise, the requested data will not be
         * passed in to the promise's callback. Instead that callback will received
         * a `null` value and the requested values will be set on the specified
         * properties of the object passed in. Those values can be accessed there
         * when the returned promise is fulfilled, as in the following code:
         *
         *     myService.getObjectProperties(myObject, "x", "y").then(function () {
         *         someFunction(myObject.x, myObject.y);
         *     }
         *
         * @method
         * @argument {object} object          - The object whose property values are
         *                                      being requested.
         * @argument {string[]} propertyNames - The names of each of the properties
         *                                      whose values are being requested.
         *                                      These can be provided as an array of
         *                                      strings or as a list of string
         *                                      arguments following the object
         *                                      argument.
         * @returns {external:Promise} - A promise fulfilled when all of the
         * requested data has been received and set on the specified properties of
         * the passed in object.
         */
        getObjectProperties: {
            value: function (object, propertyNames) {
                 if (this.isRootService) {
                    // Get the data, accepting property names as an array or as a list
                    // of string arguments while avoiding the creation of any new array.
                    var names = Array.isArray(propertyNames) ? propertyNames : arguments,
                        start = names === propertyNames ? 0 : 1;
                    return this._getOrUpdateObjectProperties(object, names, start, false);
                }
                else {
                    return this.rootService.getObjectProperties(object, propertyNames);
                }
            }
        },

        getObjectPropertyExpressions: {
            value: function (object, propertyValueExpressions) {
                if (this.isRootService) {
                    // Get the data, accepting property names as an array or as a list
                    // of string arguments while avoiding the creation of any new array.
                    var expressions = Array.isArray(propertyValueExpressions) ? propertyValueExpressions : arguments,
                        start = expressions === propertyValueExpressions ? 0 : 1,
                        promises = [],
                        self = this;

                    expressions.forEach(function (expression) {
                        var split = expression.split(".");
                        // if (split.length == 1) {
                        //     promises.push(self.getObjectProperties(object, split[0]));
                        // } else {
                            promises.push(self._getPropertiesOnPath(object, split));
                        // }

                    });

                    return Promise.all(promises);

                } else {
                    return this.rootService.getObjectPropertyExpressions(object, propertyValueExpressions);
                }
            }
        },

        _getPropertiesOnPath: {
            value: function (object, propertiesToRequest) {
                var self = this,
                    propertyName = propertiesToRequest.shift(),
                    promise = this.getObjectProperties(object, propertyName);

                if (promise) {
                    return promise.then(function () {
                        var result = null;
                        if (propertiesToRequest.length && object[propertyName]) {
                            result = self._getPropertiesOnPath(object[propertyName], propertiesToRequest);
                        }
                        return result;
                    });
                } else {
                    return this.nullPromise;
                }
            }
        },

        /**
         * Request possibly asynchronous values of a data object's properties,
         * forcing asynchronous values to be re-fetched and updated even if they
         * had previously been fetched or set.
         *
         * Except for the forced update, this method behaves exactly like
         * [getObjectProperties()]{@link DataService#getObjectProperties}.
         *
         * Since root services are responsible for determining when to fetch or
         * update data objects values, subclasses whose instances will not be root
         * services should override this method to call their root service's
         * implementation of it.
         *
         * Subclasses should define how property values are obtained by overriding
         * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead
         * of this method. That method will be called by this method when needed.
         *
         * @method
         * @argument {object} object          - The object whose property values are
         *                                      being requested.
         * @argument {string[]} propertyNames - The names of each of the properties
         *                                      whose values are being requested.
         *                                      These can be provided as an array of
         *                                      strings or as a list of string
         *                                      arguments following the object
         *                                      argument.
         * @returns {external:Promise} - A promise fulfilled when all of the
         * requested data has been received and set on the specified properties of
         * the passed in object.
         */
        updateObjectProperties: {
            value: function (object, propertyNames) {
                  if (this.isRootService) {
                    // Get the data, accepting property names as an array or as a list
                    // of string arguments while avoiding the creation of any new array.
                    var names = Array.isArray(propertyNames) ? propertyNames : arguments,
                        start = names === propertyNames ? 0 : 1;
                    return this._getOrUpdateObjectProperties(object, names, start, true);
                  }
                  else {
                    return this.rootService.updateObjectProperties(object, propertyNames);
                  }
            }
        },

        /**
         * Fetch the value of a data object's property, possibly asynchronously.
         *
         * The default implementation of this method delegates the fetching to a
         * child services, or does nothing but return a fulfilled promise for `null`
         * if no child service can be found to handle the specified object.
         *
         * [Raw data service]{@link RawDataService} subclasses should override
         * this method to perform any fetch or other operation required to get the
         * requested data. The subclass implementations of this method should use
         * only [fetchData()]{@link DataService#fetchData} calls to fetch data.
         *
         * This method should never be called directly:
         * [getObjectProperties()]{@link DataService#getObjectProperties} or
         * [updateObjectProperties()]{@link DataService#updateObjectProperties}
         * should be called instead as those methods handles some required caching,
         * fetch aggregation, and [data trigger]{@link DataTrigger}. Those methods
         * will call this method if and when that is necessary.
         *
         * Like the promise returned by
         * [getObjectProperties()]{@link DataService#getObjectProperties}, the
         * promise returned by this method should not pass the requested value to
         * its callback: That value must instead be set on the object passed in to
         * this method.
         *
         * @method
         * @argument {object} object   - The object whose property value is being
         *                               requested.
         * @argument {string} name     - The name of the single property whose value
         *                               is being requested.
         * @returns {external:Promise} - A promise fulfilled when the requested
         * value has been received and set on the specified property of the passed
         * in object.
         */
        fetchObjectProperty: {
            value: function (object, propertyName) {
                var isHandler = this.parentService && this.parentService._getChildServiceForObject(object) === this,
                    useDelegate = isHandler && typeof this.fetchRawObjectProperty === "function",
                    delegateFunction = !useDelegate && isHandler && this._delegateFunctionForPropertyName(propertyName),
                    propertyDescriptor = !useDelegate && !delegateFunction && isHandler && this._propertyDescriptorForObjectAndName(object, propertyName),
                    childService = !isHandler && this._getChildServiceForObject(object);

                return  useDelegate ?                       this.fetchRawObjectProperty(object, propertyName) :
                        delegateFunction ?                  delegateFunction.call(this, object) :
                        isHandler && propertyDescriptor ?   this._fetchObjectPropertyWithPropertyDescriptor(object, propertyName, propertyDescriptor) :
                        childService ?                      childService.fetchObjectProperty(object, propertyName) :
                                                            this.nullPromise;
            }
        },

        _delegateFunctionForPropertyName: {
            value: function (propertyName) {
                var capitalized = propertyName.charAt(0).toUpperCase() + propertyName.slice(1),
                    functionName = "fetch" + capitalized + "Property";
                return typeof this[functionName] === "function" && this[functionName];
            }
        },

        _fetchObjectPropertyWithPropertyDescriptor: {
            value: function (object, propertyName, propertyDescriptor) {
                var self = this,
                    objectDescriptor = propertyDescriptor.owner,
                    mapping = objectDescriptor && this.mappingWithType(objectDescriptor),
                    data = {};

                if (mapping) {

                    Object.assign(data, this.snapshotForObject(object));

                    return mapping.mapObjectToCriteriaSourceForProperty(object, data, propertyName).then(function() {
                        Object.assign(data, self.snapshotForObject(object));
                        return mapping.mapRawDataToObjectProperty(data, object, propertyName);
                    });
                } else {
                    return this.nullPromise;
                }

                //return mapping.
                // (object,{}, propertyName);

            }
        },

        /**
         * @private
         * @method
         */
        _getOrUpdateObjectProperties: {
            value: function (object, names, start, isUpdate) {
                var triggers, trigger, promises, promise, i, n;
                // Request each data value separately, collecting unique resulting
                // promises into an array and a set, but avoid creating any array
                // or set unless that's necessary.
                triggers = this._getTriggersForObject(object);
                for (i = start, n = names.length; i < n; i += 1) {
                    trigger = triggers && triggers[names[i]];
                    promise = !trigger ? this.nullPromise :
                              isUpdate ? trigger.updateObjectProperty(object) :
                                         trigger.getObjectProperty(object);
                    if (promise !== this.nullPromise) {
                        if (!promises) {
                            promises = {array: [promise]};
                        } else if (!promises.set && promises.array[0] !== promise) {
                            promises.set = new Set();
                            promises.set.add(promises.array[0]);
                            promises.set.add(promise);
                            promises.array.push(promise);
                        } else if (promises.set && !promises.set.has(promise)) {
                            promises.set.add(promise);
                            promises.array.push(promise);
                        }
                    }
                }
                // if (names.indexOf("geometryType")) {
                //
                // }
                // Return a promise that will be fulfilled only when all of the
                // requested data has been set on the object. If possible do this
                // without creating any additional promises.
                return !promises ?     this.nullPromise :
                       !promises.set ? promises.array[0] :
                                       Promise.all(promises.array).then(this.nullFunction);
            }
        },

        /***************************************************************************
         * Data Object Creation
         */

        /**
         * Find an existing data object corresponding to the specified raw data, or
         * if no such object exists, create one.
         *
         * Since root services are responsible for tracking and creating data
         * objects, subclasses whose instances will not be root services should
         * override this method to call their root service's implementation of it.
         *
         * @method
         * @argument {DataObjectDescriptor} type - The type of object to find or
         *                                         create.
         * @argument {Object} data               - An object whose property values
         *                                         hold the object's raw data. That
         *                                         data will be used to determine
         *                                         the object's unique identifier.
         * @argument {?} context                 - A value, usually passed in to a
         *                                         [raw data service's]{@link RawDataService}
         *                                         [addRawData()]{@link RawDataService#addRawData}
         *                                         method, that can help in getting
         *                                         or creating the object.
         * @returns {Object} - The existing object with the unique identifier
         * specified in the raw data, or if no such object exists a newly created
         * object of the specified type.
         */
        getDataObject: {
            value: function (type, data, context, dataIdentifier) {
                if (this.isRootService) {
                    var dataObject;
                    // TODO [Charles]: Object uniquing.
                    if (this.isUniquing && dataIdentifier) {
                        dataObject = this.objectForDataIdentifier(dataIdentifier);
                    }
                    if (!dataObject) {
                        dataObject = this._createDataObject(type, dataIdentifier);
                    }

                    return dataObject;
                }
                else {
                    return this.rootService.getDataObject(type, data, context, dataIdentifier);
                }

            }
        },

        isUniquing: {
            value: false
        },

        _identifier: {
            value: undefined
        },

        identifier: {
            get: function() {
                return this._identifier || (this._identifier = Montage.getInfoForObject(this).moduleId);
            }
        },

        __dataIdentifierByObject: {
            value: null
        },

        _dataIdentifierByObject: {
            get: function() {
                return this.__objectsByDataIdentifier || (this.__objectsByDataIdentifier = new WeakMap());
            }
        },

        /**
         * Returns a unique object for a DataIdentifier
         * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead
         * of this method. That method will be called by this method when needed.
         *
         * @method
         * @argument {object} object         - The object whose property values are
         *                                      being requested.
         *
         * @returns {DataIdentifier}        - An object's DataIdentifier
         */
         dataIdentifierForObject: {
            value: function(object) {
                return this._dataIdentifierByObject.get(object);
            }
        },

        /**
         * Records an object's DataIdentifier
         *
         * @method
         * @argument {object} object                        - an Object.
         * @argument {DataIdentifier} dataIdentifier        - The object whose property values are
         */
        recordDataIdentifierForObject: {
            value: function(dataIdentifier, object) {
                this._dataIdentifierByObject.set(object, dataIdentifier);
            }
        },

        /**
         * Remove an object's DataIdentifier
         *
         * @method
         * @argument {object} object         - an object
         */
        removeDataIdentifierForObject: {
            value: function(object) {
                this._dataIdentifierByObject.delete(object);
            }
        },

        __objectByDataIdentifier: {
            value: null
        },

        _objectByDataIdentifier: {
            get: function() {
                return this.__objectByDataIdentifier || (this.__objectByDataIdentifier = new WeakMap());
            }
        },
        /**
         *  Returns a unique object for a DataIdentifier
         * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead
         * of this method. That method will be called by this method when needed.
         *
         * @method
         * @argument {object} object        - object
         * @returns {DataIdentifier}        - object's DataIdentifier
         */
        objectForDataIdentifier: {
            value: function(dataIdentifier) {
                return this._objectByDataIdentifier.get(dataIdentifier);
            }
        },
        /**
         * Records an object's DataIdentifier
         *
         * @method
         * @argument {DataIdentifier} dataIdentifier    - DataIdentifier
         * @argument {object} object                    - object represented by dataIdentifier
         */
        recordObjectForDataIdentifier: {
            value: function(object, dataIdentifier) {
                this._objectByDataIdentifier.set(dataIdentifier, object);
            }
        },

        /**
         * Remove an object's DataIdentifier
         *
         * @method
         * @argument {object} object         - an object
         */
        removeObjectForDataIdentifier: {
            value: function(dataIdentifier) {
                this._objectByDataIdentifier.delete(dataIdentifier);
            }
        },

        /**
         * Create a new data object of the specified type.
         *
         * Since root services are responsible for tracking and creating data
         * objects, subclasses whose instances will not be root services should
         * override this method to call their root service's implementation of it.
         *
         * @method
         * @argument {DataObjectDescriptor} type - The type of object to create.
         * @returns {Object}                     - The created object.
         */
        //TODO add the creation of a temporary identifier to pass to _createDataObject
        createDataObject: {
            value: function (type) {
                if (this.isRootService) {
                    var object = this._createDataObject(type);
                    this.createdDataObjects.add(object);
                    return object;
                } else {
                    this.rootService.createDataObject(type);
                }
            }
        },

        /**
         * Create a data object without registering it in the new object map.
         *
         * @private
         * @method
         * @argument {DataObjectDescriptor} type - The type of object to create.
         * @returns {Object}                     - The created object.
         */
        _createDataObject: {
            value: function (type, dataIdentifier) {
                var objectDescriptor = this._objectDescriptorForType(type),
                    object = Object.create(this._getPrototypeForType(objectDescriptor));
                if (object) {

                    //This needs to be done before a user-land code can attempt to do
                    //anyting inside its constructor, like creating a binding on a relationships
                    //causing a trigger to fire, not knowing about the match between identifier
                    //and object... If that's feels like a real situation, it is.
                    this.registerUniqueObjectWithDataIdentifier(object, dataIdentifier);
                    // if (dataIdentifier && this.isUniquing) {
                    //     this.recordDataIdentifierForObject(dataIdentifier, object);
                    //     this.recordObjectForDataIdentifier(object, dataIdentifier);
                    // }

                    object = object.constructor.call(object) || object;
                    if (object) {
                        this._setObjectType(object, objectDescriptor);
                    }
                }
                return object;
            }
        },

         /**
         * Register an object with its dataIdentifier for uniquing reasons
         *
         * @private
         * @method
         * @argument {Object} object - object to register.
         * @argument {DataIdentifier} dataIdentifier - dataIdentifier of object to register.
         * @returns {void}
         */
       registerUniqueObjectWithDataIdentifier: {
            value: function(object, dataIdentifier) {
                if (object && dataIdentifier && this.isRootService && this.isUniquing) {
                    this.recordDataIdentifierForObject(dataIdentifier, object);
                    this.recordObjectForDataIdentifier(object, dataIdentifier);
                }
            }
        },

        /***************************************************************************
         * Data Object Changes
         */

        /**
         * A set of the data objects created by this service or any other descendent
         * of this service's [root service]{@link DataService#rootService} since
         * that root service's data was last saved, or since the root service was
         * created if that service's data hasn't been saved yet.
         *
         * Since root services are responsible for tracking data objects, subclasses
         * whose instances will not be root services should override this property
         * to return their root service's value for it.
         *
         * @type {Set.<Object>}
         */
        createdDataObjects: {
            get: function () {
                if (this.isRootService) {
                    if (!this._createdDataObjects) {
                        this._createdDataObjects = new Set();
                    }
                    return this._createdDataObjects;
                }
                else {
                    return this.rootService.createdDataObjects;
                }
            }
        },

        /**
         * A set of the data objects managed by this service or any other descendent
         * of this service's [root service]{@link DataService#rootService} that have
         * been changed since that root service's data was last saved, or since the
         * root service was created if that service's data hasn't been saved yet
         *
         * Since root services are responsible for tracking data objects, subclasses
         * whose instances will not be root services should override this property
         * to return their root service's value for it.
         *
         * @type {Set.<Object>}
         */
        changedDataObjects: {
            get: function () {
                if (this.isRootService) {
                    this._changedDataObjects = this._changedDataObjects || new Set();
                    return this._changedDataObjects;
                }
                else {
                    return this.rootService.changedDataObjects();
                }
            }
        },

        _changedDataObjects: {
            value: undefined
        },

        /***************************************************************************
         * Fetching Data
         */

        /**
         * Fetch data from the service using its child services.
         *
         * This method accept [types]{@link DataObjectDescriptor} as alternatives to
         * [queries]{@link DataQuery}, and its [stream]{DataStream} argument is
         * optional, but when it calls its child services it will provide them with
         * a [query]{@link DataQuery}, it provide them with a
         * [stream]{DataStream}, creating one if necessary, and the stream will
         * include a reference to the query. Also, if a child service's
         * implementation of this method return `undefined` or `null`, this method
         * will return the stream passed in to the call to that child.
         *
         * The requested data may be fetched asynchronously, in which case the data
         * stream will be returned immediately but the stream's data will be added
         * to the stream at a later time.
         *
         * @method
         * @argument {DataQuery|DataObjectDescriptor|ObjectDescriptor|Function|String}
         *           queryOrType   - If this argument's value is a query
         *                              it will define what type of data should
         *                              be returned and what criteria that data
         *                              should satisfy. If the value is a type
         *                              it will only define what type of data
         *                              should be returned, and the criteria
         *                              that data should satisfy can be defined
         *                              using the `criteria` argument.  A type
         *                              is defined as either a DataObjectDesc-
         *                              riptor, an Object Descriptor, a Construct-
         *                              or the string module id.  The method will
         *                              convert the passed in type to a Data-
         *                              ObjectDescriptor (deprecated) or an
         *                              ObjectDescriptor.  This is true whether
         *                              passing in a DataQuery or a type.
         * @argument {?Object}
         *           optionalCriteria - If the first argument's value is a
         *                              type this argument can optionally be
         *                              provided to defines the criteria which
         *                              the returned data should satisfy.
         *                              If the first argument's value is a
         *                              query this argument should be
         *                              omitted and will be ignored if it is
         *                              provided.
         * @argument {?DataStream}
         *           optionalStream   - The stream to which the provided data
         *                              should be added. If no stream is
         *                              provided a stream will be created and
         *                              returned by this method.
         * @returns {?DataStream} - The stream to which the fetched data objects
         * were or will be added, whether this stream was provided to or created by
         * this method.
         */
        fetchData: {
            value: function (queryOrType, optionalCriteria, optionalStream) {
                var self = this,
                    isSupportedType = !(queryOrType instanceof DataQuery),
                    type = isSupportedType && queryOrType,
                    criteria = optionalCriteria instanceof DataStream ? undefined : optionalCriteria,
                    query = type ? DataQuery.withTypeAndCriteria(type, criteria) : queryOrType,
                    stream = optionalCriteria instanceof DataStream ? optionalCriteria : optionalStream;

                // make sure type is an object descriptor or a data object descriptor.
                query.type = this._objectDescriptorForType(query.type);
                // Set up the stream.
                stream = stream || new DataStream();
                stream.query = query;
                stream.dataExpression = query.selectExpression;

                this._dataServiceByDataStream.set(stream, this._childServiceRegistrationPromise.then(function() {
                    var service;
                    //This is a workaround, we should clean that up so we don't
                    //have to go up to answer that question. The difference between
                    //.TYPE and Objectdescriptor still creeps-in when it comes to
                    //the service to answer that to itself
                    if (self.parentService && self.parentService.childServiceForType(query.type) === self && typeof self.fetchRawData === "function") {
                        service = self;
                        service._fetchRawData(stream);
                    } else {

                        // Use a child service to fetch the data.
                        try {

                            service = self.childServiceForType(query.type);
                            if (service) {
                                stream = service.fetchData(query, stream) || stream;
                                self._dataServiceByDataStream.set(stream, service);
                            } else {
                                throw new Error("Can't fetch data of unknown type - " + (query.type.typeName || query.type.name) + "/" + query.type.uuid);
                            }
                        } catch (e) {
                            stream.dataError(e);
                        }
                    }

                    return service;
                }));
                // Return the passed in or created stream.
                return stream;
            }
        },

        _fetchRawData: {
            value: function (stream) {
                var self = this,
                    childService = this._childServiceForQuery(stream.query);

                if (childService) {
                    childService._fetchRawData(stream);
                } else {
                    if (this.authorizationPolicy === AuthorizationPolicy.ON_DEMAND) {
                        if (typeof this.shouldAuthorizeForQuery === "function" && this.shouldAuthorizeForQuery(stream.query) && !this.authorization) {
                            this.authorizationPromise = exports.DataService.authorizationManager.authorizeService(this).then(function(authorization) {
                                self.authorization = authorization;
                                return authorization;
                            }).catch(function(error) {
                                console.log(error);
                            });
                        }
                    }
                    this.authorizationPromise.then(function (authorization) {
                        var streamSelector = stream.query;
                        stream.query = self.mapSelectorToRawDataQuery(streamSelector);
                        self.fetchRawData(stream);
                        stream.query = streamSelector;
                    });
                }
            }
        },

        _childServiceForQuery: {
            value: function (query) {
                var serviceModuleID = this._serviceIdentifierForQuery(query),
                    service = serviceModuleID && this._childServicesByIdentifier.get(serviceModuleID);

                if (!service && this._childServicesByType.has(query.type)) {
                    service = this._childServicesByType.get(query.type);
                    service = service && service[0];
                }

                return service || null;
            }
        },

        _serviceIdentifierForQuery: {
            value: function (query) {
                var parameters = query.criteria.parameters,
                    serviceModuleID = parameters && parameters.serviceIdentifier,
                    mapping, propertyName;

                if (!serviceModuleID) {
                    mapping = this.mappingWithType(query.type);
                    propertyName = mapping && parameters && parameters.propertyName;
                    serviceModuleID = propertyName && mapping.serviceIdentifierForProperty(propertyName);
                }

                return serviceModuleID;
            }
        },

        __dataServiceByDataStream: {
            value: null
        },

        _dataServiceByDataStream: {
            get: function() {
                return this.__dataServiceByDataStream || (this.__dataServiceByDataStream = new WeakMap());
            }
        },

        dataServiceForDataStream: {
            get: function(dataStream) {
                return this._dataServiceByDataStream.get(dataStream);
            }
        },

        /**
         * To be called to indicates that the consumer has lost interest in the passed DataStream.
         * This will allow the RawDataService feeding the stream to take appropriate measures.
         *
         * @method
         * @argument {DataStream} [dataStream] - The DataStream to cancel
         * @argument {Object} [reason] - An object indicating the reason to cancel.
         *
         */
        cancelDataStream: {
            value: function (dataStream, reason) {
                if (dataStream) {
                  var  rawDataService = this._dataServiceByDataStream.get(dataStream),
                    self = this;

                  if (Promise.is(rawDataService)) {
                        rawDataService.then(function(service) {
                            self._cancelServiceDataStream(service, dataStream, reason);
                        });
                    }
                    else {
                        this._cancelServiceDataStream(rawDataService, dataStream, reason);
                    }
                }

            }
        },

        _cancelServiceDataStream: {
            value: function (rawDataService, dataStream, reason) {
                rawDataService.cancelRawDataStream(dataStream, reason);
                this._dataServiceByDataStream.delete(dataStream);
            }
        },

        /***************************************************************************
         * Saving Data
         */

        /**
         * Delete a data object.
         *
         * @method
         * @argument {Object} object - The object whose data should be deleted.
         * @returns {external:Promise} - A promise fulfilled when the object has
         * been deleted.
         */
        deleteDataObject: {
            value: function (object) {
                var saved = !this.createdDataObjects.has(object);
                return this._updateDataObject(object, saved && "deleteDataObject");
            }
        },

        /**
         * Save changes made to a data object.
         *
         * @method
         * @argument {Object} object - The object whose data should be saved.
         * @returns {external:Promise} - A promise fulfilled when all of the data in
         * the changed object has been saved.
         */
        saveDataObject: {
            value: function (object) {
                //return this._updateDataObject(object, "saveDataObject");

                var self = this,
                    service,
                    promise = this.nullPromise,
                    mappingPromise;

                if (this.parentService && this.parentService._getChildServiceForObject(object) === this) {
                    var record = {};
                    mappingPromise =  this._mapObjectToRawData(object, record);
                    if (!mappingPromise) {
                        mappingPromise = this.nullPromise;
                    }
                    return mappingPromise.then(function () {
                            return self.saveRawData(record, object)
                                .then(function () {
                                    self.rootService.createdDataObjects.delete(object);
                                    return null;
                                });
                     });
                }
                else {
                    service = this._getChildServiceForObject(object);
                    if (service) {
                        return service.saveDataObject(object);
                    }
                    else {
                        return promise;
                    }
                }
            }
        },

        _updateDataObject: {
            value: function (object, action) {
                var self = this,
                    service,
                    promise = this.nullPromise;

                if (this.parentService && this.parentService._getChildServiceForObject(object) === this) {
                    service = action && this;
                }
                else {
                    service = action && this._getChildServiceForObject(object);
                    if (service) {
                        return service._updateDataObject(object, action);
                    }
                }

                if (!action) {
                    self.createdDataObjects.delete(object);
                } else if (service) {
                    promise = service[action](object).then(function () {
                        self.createdDataObjects.delete(object);
                        return null;
                    });
                }
                return promise;
            }
        },

        _saveDataObject: {
            value: function (object) {
                var record = {};
                this._mapObjectToRawData(object, record);
                return this.saveRawData(record, object);
            }
        },
        // _updateDataObject: {
        //     value: function (object, action) {
        //         var self = this,
        //             service = action && this._getChildServiceForObject(object),
        //             promise = this.nullPromise;

        //         if (!action) {
        //             self.createdDataObjects.delete(object);
        //         } else if (service) {
        //             promise = service[action](object).then(function () {
        //                 self.createdDataObjects.delete(object);
        //                 return null;
        //             });
        //         }
        //         return promise;
        //     }
        // },

        /***************************************************************************
         * Offline
         */

        _initializeOffline: {
            value: function () {
                // TODO: This code assumes that the first instance of DataService or
                // of one of its subclasses is either the
                // root service, and that no instance of DataService subclasses are.
                // This needs to be fixed to allow DataService child services and
                // DataService subclass root services.
                var self = this;
                if (
                    typeof global.addEventListener === 'function' &&
                        !exports.DataService.prototype._isOfflineInitialized
                ) {
                    exports.DataService.prototype._isOfflineInitialized = true;
                    global.addEventListener('online', function (event) {
                        self.rootService.isOffline = false;
                    });
                    global.addEventListener('offline', function (event) {
                        self.rootService.isOffline = true;
                    });
                }
            }
        },

        _isOfflineInitialized: {
            value: false
        },

        /**
         * Returns a value derived from and continuously updated with the value of
         * [navigator.onLine]{@link https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine}.
         *
         * Root services are responsible for tracking offline status, and subclasses
         * not designed to be root services should override this property to get
         * its value from their root service.
         *
         * @type {boolean}
         */
        isOffline: {
            get: function () {
                if (this._isOffline === undefined) {
                    // Determine the initial value from the navigator state and call
                    // the public setter so _goOnline() is invoked if appropriate.
                    this.isOffline = !navigator.onLine;
                }
                return this._isOffline;
            },
            set: function (offline) {
                var self = this;
                if (this._willBeOffline === null) {
                    // _goOnline() just finished, set _isOffline to the desired
                    // value and clear the "just finished" flag in _willBeOffline.
                    this._isOffline = offline ? true : false;
                    this._willBeOffline = undefined;
                } else if (this._willBeOffline !== undefined) {
                    // _goOnline() is in progress, just record the future value.
                    this._willBeOffline = offline ? true : false;
                } else if (this._isOffline === false) {
                    // Already online and not starting up, no need for _goOnline().
                    this._isOffline = offline ? true : false;
                } else if (!offline) {
                    // Going from offline to online, or starting up online, so
                    // assume we were last offline, call _goOnline(), and only
                    // change the value  when that's done.
                    this._isOffline = true;
                    this._willBeOffline = false;
                    this._goOnline().then(function () {
                        var offline = self._willBeOffline;
                        self._willBeOffline = null;
                        self.isOffline = offline;
                        return null;
                    });
                }
            }
        },

        _isOffline: {
            // `undefined` on startup, otherwise always `true` or `false`.
            value: false
        },

        _willBeOffline: {
            // `true` or `false` while _goOnline() is in progress, `null` just after
            // it's done, `undefined` otherwise.
            value: undefined
        },

        _goOnline: {
            value: function() {
                var self = this;
                return this.readOfflineOperations().then(function (operations) {
                    operations.sort(this._compareOfflineOperations);
                    return self.performOfflineOperations(operations);
                }).catch(function (e) {
                    console.error(e);
                });
            }
        },

        _compareOfflineOperations: {
            value: function(operation1, operation2) {
                // TODO: Remove reference to `lastModified` once child services have
                // been udpated to use `time` instead.
                return operation1.lastModified < operation2.lastModified ?   -1 :
                       operation1.lastModified > operation2.lastModified ?   1 :
                       operation1.time < operation2.time ?                   -1 :
                       operation1.time > operation2.time ?                   1 :
                       operation1.index < operation2.index ?                 -1 :
                       operation1.index > operation2.index ?                 1 :
                                                                             0;
            }
        },

        /**
         * Reads all the offline operations recorded on behalf of this service.
         *
         * The default implementation aggregates this service children's offline
         * operations, keeping track of which child service is responsible for each
         * operation.
         *
         * Subclasses that provide offline support should override this method to
         * return the operations that have been performed while offline.
         *
         * @method
         */
        readOfflineOperations: {
            value: function () {
                // TODO: Get rid of the dummy WeakMap passed to children once the
                // children's readOfflineOperations code has been updated to not
                // expect it.
                // This implementation avoids creating promises for services with no
                // children or whose children don't have offline operations.
                var self = this,
                    dummy = new WeakMap(),
                    services = this._offlineOperationServices,
                    array, promises;
                this.childServices.forEach(function (child) {
                    var promise = child.readOfflineOperations(dummy);
                    if (promise !== self.emptyArrayPromise) {
                        array = array || [];
                        promises = promises || [];
                        promises.push(promise.then(function(operations) {
                            var i, n;
                            for (i = 0, n = operations && operations.length; i < n; i += 1) {
                                services.set(operations[i], child);
                                array.push(operations[i]);
                            }
                            return null;
                        }));
                    }
                });
                return promises ? Promise.all(promises).then(function () { return array; }) :
                                  this.emptyArrayPromise;
            }
        },

        /**
         * @private
         * @type {Map<DataOperation, DataService>}
         */
        _offlineOperationServices: {
            get:function() {
                if (!this.__offlineOperationServices) {
                    this.__offlineOperationServices = new WeakMap();
                }
                return this.__offlineOperationServices;
            }
        },

        __offlineOperationServices: {
            value: undefined
        },

        /**
         * Perform operations recorded while offline. This will be invoked when the
         * service comes online after being offline.
         *
         * The default implementation delegates performance of each operation to
         * the child service responsible for that operation, as determined by
         * [readOfflineOperations()]{@link DataService#readOfflineOperations}. It
         * will batch operations if several consecutive operations belong to the
         * same child service.
         *
         * For each operation not handled by a child service, the default
         * implementation calls a method named `performFooOfflineOperation()`, if
         * such a method exists in this service where `foo` is the operation's
         * [data type]{@link DataOperation#dataType}. If no such method exists,
         * [readOfflineOperation()]{@link DataService#readOfflineOperation} is
         * called instead.
         *
         * Subclasses that provide offline support should implement these
         * `performFooOfflineOperation()` methods or override the
         * `readOfflineOperation()` method to perform each operation, or they can
         * override this `performOfflineOperations()` method instead.
         *
         * Subclass overriding this method are responsible for
         * [deleting]{@link DataService#deleteOfflineOperations} operations after
         * they have been performed. Subclasses implementing
         * `performFooOfflineOperation()` methods or overriding the
         * `readOfflineOperation()` method are not.
         *
         * @method
         * @argument {Array.<DataOperation>} - operations
         * @returns {Promise} - A promise fulfilled with a null value when the
         * operations have been performed, or rejected if a problem occured that
         * should prevent following operations from being performed.
         */
        performOfflineOperations: {
            value: function(operations) {
                var services = this._offlineOperationServices,
                    promise = this.nullPromise,
                    child,
                    i, j, n, jOperation, jOperationChanges, jService, jOperationType, jTableSchema, jForeignKeys,
                    OfflineService = OfflineService,
                    k, countK, kForeignKey,kOnlinePrimaryKey;

                // Perform each operation, batching if possible, and collecting the
                // results in a chain of promises.
                for (i = 0, n = operations.length; i < n; i = j) {
                    // Find the service responsible for this operation.
                    child = services.get(operations[i]);
                    // Find the end of a batch of operations for this service.
                    j = i + 1;
                    while (j < n && child && (jService = services.get((jOperation = operations[j]))) === child) {
                        ++j;
                    }
                    // Add the promise to perform this batch of operations to the
                    // end of the chain of promises to fulfill all operations.
                    promise =
                        this._performOfflineOperationsBatch(promise, child, operations, i, j);
                }
                // Return a promise for the sequential fulfillment of all operations.
                return promise;
            }
        },

        _performOfflineOperationsBatch: {
            value: function(promise, child, operations, start, end) {
                var self = this;
                return promise.then(function () {
                    return child ?
                        child.performOfflineOperations(operations.slice(start, end)) :
                            self._performAndDeleteOfflineOperation(operations[start]);
                });
            }
        },

        _performAndDeleteOfflineOperation: {
            value: function(operation) {
                //Before we perform an operation, we need to look a foreignKeys in jOperation changes to update if needed before performing the operation
                //if we don't have a known list of foreign keys, we'll consider all potential candidate
                var self = this,
                    operationType = operation.type,
                    tableSchema, foreignKeys,
                    k, countK, kOnlinePrimaryKey, kForeignKey;

                if (this.offlineService) {
                    tableSchema = this.offlineService.schema[operationType];
                    foreignKeys = tableSchema.foreignKeys;
                }

                if (!foreignKeys) {
                    foreignKeys = tableSchema._computedForeignKeys ||
                        (tableSchema._computedForeignKeys = Object.keys(operation.changes));
                }

                for (k=0, countK = foreignKeys.length;k<countK;k++) {
                    kForeignKey = foreignKeys[k];
                    //If a previous operation resulted in an online primaryKey replacing an offline one,
                    //we update the operation's changes accordingly
                    if ((kOnlinePrimaryKey = this.onlinePrimaryKeyForOfflinePrimaryKey(operation.changes[kForeignKey]))) {
                        operation.changes[kForeignKey] = kOnlinePrimaryKey;
                    }
                }

                return this._performTypedOfflineOperation(operation).then(function () {
                    return self.deleteOfflineOperations([operation]);
                });
            }
        },

        _performTypedOfflineOperation: {
            value: function(operation) {
                // TODO: Remove support for operation.type once all child services
                // have been updated to provide an operation.dataType instead.
                var type = operation.dataType || operation.type,
                    method = type && this[this._getOfflineOperationMethodName(type)];
                return typeof(method) === "function" ? method.call(this, operation) :
                                                       this.performOfflineOperation(operation);
            }
        },

        _getOfflineOperationMethodName: {
            value: function(type) {
                var isString = typeof type === "string",
                    name = isString && this._offlineOperationMethodNames.get(type);
                if (isString && !name) {
                    name = "perform";
                    name += type[0].toUpperCase();
                    name += type.slice(1);
                    name += "OfflineOperation";
                    this._offlineOperationMethodNames.set(type, name);
                }
                return name;
            }
        },

        _offlineOperationMethodNames: {
            value: new Map()
        },

        /**
         * Called from
         * [performOfflineOperations()]{@link DataService#performOfflineOperations}
         * to perform a particular operation when no more specific
         * `performFooOfflineOperation()` method is available for that operation,
         * where `Foo` is the operation's [data type]{@link DataOperation#dataType}.
         *
         * The default implementation does nothing.
         *
         * Subclass overriding this method do not need to
         * [delete]{@link DataService#deleteOfflineOperations} the passed in
         * operation after it has successfully been performed: The method calling
         * this method will take care of that.
         *
         * @method
         * @argument {DataOperation} operation
         * @returns {Promise} - A promise fulfilled with a null value when the
         * operation has been performed, or rejected if a problem occured that
         * should prevent following operations from being performed.
         */
        performOfflineOperation: {
            value: function(operation) {
                // To be overridden by subclasses that use offline operations.
                return this.nullPromise;
            }
        },

        // To be overridden by subclasses as necessary
        onlinePrimaryKeyForOfflinePrimaryKey: {
            value: function(offlinePrimaryKey) {
                return this.offlineService ?
                    this.offlineService.onlinePrimaryKeyForOfflinePrimaryKey(offlinePrimaryKey) : null;
            }
        },

        /**
         * Delete operations recorded while offline.
         *
         * Services overriding the (plural)
         * [performOfflineOperations()]{@link DataService#performOfflineOperations}
         * method must invoke this method after each operation they perform is
         * successfully performed.
         *
         * This method will be called automatically for services that perform
         * operations by implementing a
         * [performOfflineOperation()]{@link DataService#performOfflineOperation}
         * or `performFooOfflineOperation()` methods (where `foo` is an operation
         * [data type]{@link DataOperation#dataType}).
         *
         * Subclasses that provide offline operations support must override this
         * method to delete the specified offline operations from their records.
         *
         * @method
         * @argument {Array.<Object>} operations
         * @returns {Promise} - A promise fulfilled with a null value when the
         * operations have been deleted.
         */
        deleteOfflineOperations: {
            value: function(operations) {
                // To be overridden by subclasses that use offline operations.
                return this.nullPromise;
            }
        },

        /***************************************************************************
         * Utilities
         */

        /**
         * A function that does nothing but returns null, useful for terminating
         * a promise chain that needs to return null, as in the following code:
         *
         *     var self = this;
         *     return this.fetchSomethingAsynchronously().then(function (data) {
         *         return self.doSomethingAsynchronously(data.part);
         *     }).then(this.nullFunction);
         *
         * @type {function}
         */
        nullFunction: {
            value: function () {
                return null;
            }
        },

        /**
         * A shared promise resolved with a value of
         * `null`, useful for returning from methods like
         * [fetchObjectProperty()]{@link DataService#fetchObjectProperty}
         * when the requested data is already there.
         *
         * @type {external:Promise}
         */
        nullPromise: {
            get: function () {
                if (!exports.DataService._nullPromise) {
                    exports.DataService._nullPromise = Promise.resolve(null);
                }
                return exports.DataService._nullPromise;
            }
        },

        _nullPromise: {
            value: undefined
        },

        /**
         * @todo Document.
         */
        emptyArrayPromise: {
            get: function () {
                if (!exports.DataService._emptyArrayPromise) {
                    exports.DataService._emptyArrayPromise = Promise.resolve([]);
                }
                return exports.DataService._emptyArrayPromise;
            }
        },

        _emptyArrayPromise: {
            value: undefined
        },

        /**
         * A possibly shared promise resolved in the next cycle of the event loop
         * or soon thereafter, at which point the current event handling will be
         * complete. This is useful for services that need to buffer up actions so
         * they're committed only once in a given event loop.
         *
         * @type {external:Promise}
         */
        eventLoopPromise: {
            get: function () {
                var self = this;
                if (!this._eventLoopPromise) {
                    this._eventLoopPromise = new Promise(function (resolve, reject) {
                        setTimeout(function () {
                            self._eventLoopPromise = undefined;
                            resolve();
                        }, 0);
                    });
                }
                return this._eventLoopPromise;
            }
        },

        /**
         * Splice an array into another array.
         *
         * @method
         * @argument {Array} array   - The array to modify.
         * @argument {Array} insert  - The items to splice into that array.
         * @argument {number} index  - The index at which to splice those items, by
         *                             default `0`.
         * @argument {number} length - The number of items of the original array to
         *                             replace with items from the spliced array, by
         *                             default `array.length`.
         */
        spliceWithArray: {
            value: function (array, insert, index, length) {
                index = index || 0;
                length = length || length === 0 ? length : Infinity;
                return insert ? array.splice.apply(array, [index, length].concat(insert)) :
                                array.splice(index, length);
            }
        }

    }, /** @lends DataService */ {

        /***************************************************************************
         * Service Hierarchy
         */

        /**
         * A reference to the application's main service.
         *
         * Applications typically have one and only one
         * [root service]{@link DataService#rootService} to which all data requests
         * are sent, and this is called the application's main service. That service
         * can in turn delegate handling of different types of data to child
         * services specialized by type.
         *
         * This property will be set automatically if the {@link DataService}
         * constructor is called and if the first service created is either the
         * main service or a descendent of the main service.
         *
         * @type {DataService}
         */
        mainService: {
            get: function () {
                if (this._mainService && this._mainService.parentService) {
                    this._mainService = this._mainService.rootService;
                }
                return this._mainService;
            },
            set: function (service) {
                this._mainService = service;
            }
        },

        /***************************************************************************
         * Authorization
         */

        AuthorizationPolicyType: {
            value: AuthorizationPolicyType
        },

        AuthorizationPolicy: {
            value: AuthorizationPolicy
        },

        authorizationManager: {
            value: AuthorizationManager
        }

    });