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

var DataService = require("data/service/data-service").DataService,
    DataMapping = require("data/service/data-mapping").DataMapping,
    DataIdentifier = require("data/model/data-identifier").DataIdentifier,
    Deserializer = require("core/serialization/deserializer/montage-deserializer").MontageDeserializer,
    Map = require("collections/map"),
    Montage = require("montage").Montage,
    WeakMap = require("collections/weak-map"),
    deprecate = require("core/deprecate");

/**
 * Provides data objects of certain types and manages changes to them based on
 * "raw" data obtained from or sent to one or more other services, typically
 * REST or other network services. Raw data services can therefore be considered
 * proxies for these REST or other services.
 *
 * Raw data services are usually the children of a
 * [data service]{@link DataService} that often is the application's
 * [main data service]{@link DataService.mainService}. All calls to raw data
 * services that have parent services must be routed through those parents.
 *
 * Raw 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:
 *
 *     RawDataService.call(this);
 *
 * @class
 * @extends DataService
 */
exports.RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ {

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

    constructor: {
        value: function RawDataService() {
            DataService.call(this);
            this._typeIdentifierMap = new Map();
        }
    },

    /**
     * @deprecated
     */
    initWithModel: {
        value: function (model) {
            var self = this;
            return require.async(model).then(function (descriptor) {
                var deserializer = new Deserializer().init(JSON.stringify(descriptor), require);
                return deserializer.deserializeObject();
            }).then(function (model) {
                self.model = model;
                return self;
            });
        }
    },

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

    deserializeSelf: {
        value:function (deserializer) {

            this.super(deserializer);
        }
    },

    /*
     * The ConnectionDescriptor object where possible connections will be found
     *
     * @type {ConnectionDescriptor}
     */
    connectionDescriptor: {
        value: undefined
    },

    /*
     * The current DataConnection object used to connect to data source
     *
     * @type {DataConnection}
     */
    connection: {
        value: undefined
    },

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

    // decacheObjectProperties: {
    //     value: function (object, propertyNames) {
    //         return this.rootService.decacheObjectProperties(object, propertyNames);
    //     }
    // },

    // getObjectProperties: {
    //     value: function (object, propertyNames) {
    //         return this.rootService.getObjectProperties(object, propertyNames);
    //     }
    // },

    // updateObjectProperties: {
    //     value: function (object, propertyNames) {
    //         return this.rootService.updateObjectProperties(object, propertyNames);
    //     }
    // },

    /**
     * Fetch the value of a data object's property, possibly asynchronously.
     *
     * The default implementation of this method just return a fulfilled promise
     * for `null`. Subclasses should override this method to perform any fetch
     * or other operation required to get the requested data. The subclass
     * implementations should only use calls to their
     * [root service's]{@link DataService.rootService}
     * [fetchData()]{@link DataService#fetchData} to fetch data.
     *
     * @method
     */
    // fetchObjectProperty: {
    //     value: function (object, propertyName) {
    //         var propertyDescriptor = this._propertyDescriptorForObjectAndName(object, propertyName),
    //             destinationReference = propertyDescriptor && propertyDescriptor.valueDescriptor;
    //         return destinationReference ?   this._fetchObjectPropertyWithPropertyDescriptor(object, propertyName, propertyDescriptor) :
    //                                         this.nullPromise;
    //     }
    // },

    _propertyDescriptorForObjectAndName: {
        value: function (object, propertyName) {
            var objectDescriptor = this.objectDescriptorForObject(object);
            return objectDescriptor && objectDescriptor.propertyDescriptorForName(propertyName);
        }
    },

    _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;
            // var typeName = object.constructor.TYPE.typeName,
            //     isModel = this.model instanceof Model,
            //     isObjectDescriptor = !isModel && this.model instanceof ObjectDescriptor,
            //     isObjectsObjectDescriptor = isObjectDescriptor && this.model.name === typeName;
            // return  isModel ?                       this.model.objectDescriptorForName(typeName) :
            //     isObjectsObjectDescriptor ?     this.model :
            //         undefined;
        }
    },

    // _fetchObjectPropertyWithPropertyDescriptor: {
    //     value: function (object, propertyName, propertyDescriptor) {
    //         var self = this, moduleId, cachedDescriptor, service = this.rootService;
    //         return propertyDescriptor.valueDescriptor.then(function (objectDescriptor) {
    //             moduleId = [objectDescriptor.module.id, objectDescriptor.exportName].join("/");
    //             cachedDescriptor = self.rootService._moduleIdToObjectDescriptorMap[moduleId];
    //             var selector = DataQuery.withTypeAndCriteria(cachedDescriptor, {
    //                 // snapshot: self._snapshots.get(object),
    //                 source: object,
    //                 relationshipKey: propertyName
    //             });
    //             return service.fetchData(selector);
    //         }).then(function (data) {
    //             return self._mapObjectPropertyValue(object, propertyDescriptor, data);
    //         });
    //         // return this._objectDescriptorTypeForValueDescriptor(propertyDescriptor.valueDescriptor).then(function (type) {
    //         //     var selector = DataQuery.withTypeAndCriteria(type, {
    //         //         snapshot: self._snapshots.get(object),
    //         //         source: object,
    //         //         relationshipKey: propertyName
    //         //     });
    //         //     return service.fetchData(selector);
    //         // }).then(function (data) {
    //         //     return self._mapObjectPropertyValue(object, propertyDescriptor, data);
    //         // });
    //     }
    // },

    _mapObjectPropertyValue: {
        value: function (object, propertyDescriptor, value) {
            var propertyName = propertyDescriptor.name;
            if (propertyDescriptor.cardinality === Infinity) {
                this.spliceWithArray(object[propertyName], value);
            } else {
                object[propertyName] = value[0];
            }

            if (propertyDescriptor.inversePropertyName && value && value[0]) {
                var inverseBlueprint = this._propertyDescriptorForObjectAndName(value[0], propertyDescriptor.inversePropertyName);
                if (inverseBlueprint && inverseBlueprint.cardinality === 1) {
                    value.forEach(function (inverseObject) {
                        inverseObject[propertyDescriptor.inversePropertyName] = object;
                    });
                }
            }
            return value;
        }
    },

    _objectDescriptorTypeForValueDescriptor: {
        value: function (valueDescriptor) {
            return valueDescriptor.then(function (objectDescriptor) {
                return objectDescriptor.module.require.async(objectDescriptor.module.id);
            });
        }
    },

    /***************************************************************************
     * Data Object Creation
     *
     * If there were no mapping available in the app for this record giving use
     * a type/class/condtructor, we should create one ourselves matching what we know.
     * For a REST service it would be the name of the Entity. Otherwise we should be able
     * to treat by ip domain, a type based on concatenation of all keys returned,
     * which would define the "shape"
     *
     */

    // getDataObject: {
    //     value: function (type, data, context, dataIdentifier) {
    //         return this.rootService.getDataObject(type, data, context, dataIdentifier);
    //     }
    // },

    // createDataObject: {
    //     value: function (type) {
    //         return this.rootService.createDataObject(type);
    //     }
    // },

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

    // createdDataObjects: {
    //     get: function () {
    //         return this.rootService.createdDataObjects();
    //     }
    // },

    // changedDataObjects: {
    //     get: function () {
    //         return this.rootService.changedDataObjects();
    //     }
    // },

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

    /**
     * Fetch data from this service for its parent. This method should not be
     * called directly by anyone other than this service's parent. Calls to the
     * root service should be made to initiate data fetches.
     *
     * Subclasses may override this method to fetch the requested data, but will
     * usually override [fetchRawData()]{@link RawDataService#fetchRawData}
     * instead.
     *
     * This method must be asynchronous and return as soon as possible even if
     * it takes a while to generate the requested data objects. The data objects
     * can be generated and added to the specified stream at any point after
     * this method is called, even after it returns, with calls to
     * [addData()]{@link DataStream#addData} and
     * [dataDone()]{@link DataStream#dataDone}. Usually this will be done by
     * having [fetchRawData()]{@link RawDataService#fetchRawData} make calls to
     * [addRawData()]{@link RawDataService#addRawData} and
     * [rawDataDone()]{@link RawDataService#rawDataDone}.
     *
     * The default implementation of this method calls
     * [fetchRawData()]{@link RawDataService#fetchRawData}.
     *
     * @method
     * @argument {DataObjectDescriptor|DataQuery}
     *           typeOrSelector        - Defines what data should be returned.
     *                                   If a [type]{@link DataOjectDescriptor}
     *                                   is provided instead of a
     *                                   {@link DataQuery}, a `DataQuery`
     *                                   with the specified type and no
     *                                   [criteria]{@link DataQuery#criteria}
     *                                   will be created and used for the fetch.
     * @argument {?DataStream} stream  - 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, stream) {
    //         var isSupportedType = queryOrType instanceof DataObjectDescriptor || queryOrType instanceof ObjectDescriptor,
    //             type = isSupportedType && queryOrType,
    //             query = type && DataQuery.withTypeAndCriteria(type) || queryOrType;
    //         stream = stream || new DataStream();
    //         stream.query = query;
    //         this._fetchRawData(stream);
    //         return stream;
    //     }
    // },

    // fetchData: {
    //     value: function (queryOrType, stream) {
    //         var self = this,
    //              isSupportedType = queryOrType instanceof DataObjectDescriptor || queryOrType instanceof ObjectDescriptor,
    //             type = isSupportedType && queryOrType,
    //             query = type && DataQuery.withTypeAndCriteria(type) || queryOrType;
    //         // Set up the stream.
    //         stream = stream || new DataStream();
    //         stream.query = query;
    //             // Use a child service to fetch the data.
    //             var service;
    //             try {
    //                 service = self.childServiceForType(query.type);
    //                 if (service && service!==self) {
    //                     stream = service.fetchData(query, stream) || stream;
    //                     self._dataServiceByDataStream.set(stream, service);
    //                 } else {
    //                     if(typeof self.fetchRawData === "function") {
    //                         self._fetchRawData(stream);
    //                     }
    //                     else {
    //                         throw new Error("Can't fetch data of unknown type - " + query.type.typeName + "/" + query.type.uuid);
    //                     }
    //                 }
    //             } catch (e) {
    //                 stream.dataError(e);
    //             }
    //         // Return the passed in or created stream.
    //         return stream;
    //     }
    // },

    /**
     * Fetch the raw data of this service.
     *
     * This method should not be called directly from anyone other than this
     * service's [fetchData()]{@link RawDataService#fetchData}.
     *
     * Subclasses that don't override
     * [fetchData()]{@link RawDataService#fetchData} should override this method
     * to:
     *
     *   1. Fetch the raw records needed to generate the requested data object.
     *
     *   2. Add those records to the specified stream with calls to
     *      [addRawData()]{@link RawDataService#addRawData}.
     *
     *   3. Indicate that the fetching is done with a call to
     *      [rawDataDone()]{@link RawDataService#rawDataDone}.
     *
     * This method must be asynchronous and return as soon as possible even if
     * it takes a while to obtain the raw data. The raw data can be provided to
     * the service at any point after this method is called, even after it
     * returns, with calls to [addRawData()]{@link RawDataService#addRawData}
     * and [rawDataDone()]{@link RawDataService#rawDataDone}.
     *
     * The default implementation of this method simply calls
     * [rawDataDone()]{@link RawDataService#rawDataDone} immediately.
     *
     * @method
     * @argument {DataStream} stream - The stream to which the data objects
     *                                 corresponding to the raw data should be
     *                                 added. This stream must contain a
     *                                 reference to the selector defining what
     *                                 raw data to fetch.
     */
    // TODO, swizzling the stream's user-land selector for the rawData equivalent is not really
    // practical nor safe, we either need to keep it separately or store it on the stream under
    // rawDataQuery

    // _fetchRawData: {
    //     value: function (stream) {
    //         var self = this;
    //         this.authorizationPromise.then(function (authorization) {
    //             var streamSelector = stream.query;
    //             stream.query = self.mapSelectorToRawDataQuery(streamSelector);
    //             self.fetchRawData(stream);
    //             stream.query = streamSelector;
    //         })
    //     }
    // },

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

            //TODO [TJ] Determine purpose of this check...
            // if (childService && childService.identifier.indexOf("offline-service") === -1) {
            //     childService._fetchRawData(stream);
            // } else
            if (childService) {
                childService.fetchRawData(stream);
            } else if (query.authorization) {
                stream.query = self.mapSelectorToRawDataQuery(query);
                self.fetchRawData(stream);
                stream.query = query;
            } else if (this.authorizationPolicy === DataService.AuthorizationPolicy.NONE) {
                stream.query = self.mapSelectorToRawDataQuery(query);
                self.fetchRawData(stream);
                stream.query = query;
            } else {
                DataService.authorizationManager.authorizeService(this).then(function(authorization) {
                    self.authorization = authorization;
                    return authorization;
                }).catch(function(error) {
                    console.log(error);
                }).then(function (authorization) {
                    stream.query = self.mapSelectorToRawDataQuery(query);
                    self.fetchRawData(stream);
                    stream.query = query;
                });
            }
        }
    },

    fetchRawData: {
        value: function (stream) {
            this.rawDataDone(stream);
        }
    },

    /**
     * Called through MainService when consumer has indicated that he 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.
     *
     */
    cancelRawDataStream: {
        value: function (dataStream, reason) {
        }
    },
    /***************************************************************************
     * Saving Data
     */

    /**
     * Subclasses should override this method to delete a data object when that
     * object's raw data wouldn't be useful to perform the deletion.
     *
     * The default implementation maps the data object to raw data and calls
     * [deleteRawData()]{@link RawDataService#deleteRawData} with the data
     * object passed in as the `context` argument of that method.
     *
     * @method
     * @argument {Object} object   - The object to delete.
     * @returns {external:Promise} - A promise fulfilled when the object has
     * been deleted. The promise's fulfillment value is not significant and will
     * usually be `null`.
     */
    deleteDataObject: {
        value: function (object) {
            var record = {};
            this._mapObjectToRawData(object, record);
            return this.deleteRawData(record, object);
        }
    },

    /**
     * Subclasses should override this method to delete a data object when that
     * object's raw data would be useful to perform the deletion.
     *
     * @method
     * @argument {Object} record   - An object whose properties hold the raw
     *                               data of the object to delete.
     * @argument {?} context       - An arbitrary value sent by
     *                               [deleteDataObject()]{@link RawDataService#deleteDataObject}.
     *                               By default this is the object to delete.
     * @returns {external:Promise} - A promise fulfilled when the object's data
     * has been deleted. The promise's fulfillment value is not significant and
     * will usually be `null`.
     */
    deleteRawData: {
        value: function (record, context) {
            // Subclasses must override this.
            return this.nullPromise;
        }
    },

    /**
     * Subclasses should override this method to save a data object when that
     * object's raw data wouldn't be useful to perform the save.
     *
     * The default implementation maps the data object to raw data and calls
     * [saveRawData()]{@link RawDataService#saveRawData} with the data object
     * passed in as the `context` argument of that method.
     *
     * @method
     * @argument {Object} object   - The object to save.
     * @returns {external:Promise} - A promise fulfilled when all of the data in
     * the changed object has been saved. The promise's fulfillment value is not
     * significant and will usually be `null`.
     */
    // saveDataObject: {
    //     value: function (object) {
    //         var record = {};
    //         this._mapObjectToRawData(object, record);
    //         return this.saveRawData(record, object);
    //     }
    // },

    /**
     * Subclasses should override this method to save a data object when that
     * object's raw data would be useful to perform the save.
     *
     * @method
     * @argument {Object} record   - An object whose properties hold the raw
     *                               data of the object to save.
     * @argument {?} context       - An arbitrary value sent by
     *                               [saveDataObject()]{@link RawDataService#saveDataObject}.
     *                               By default this is the object to save.
     * @returns {external:Promise} - A promise fulfilled when the object's data
     * has been saved. The promise's fulfillment value is not significant and
     * will usually be `null`.
     */
    saveRawData: {
        value: function (record, context) {
            // Subclasses must override this.
            return this.nullPromise;
        }
    },

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

    /*
     * Returns the [root service's offline status]{@link DataService#isOffline}.
     *
     * @type {boolean}
     */
    isOffline: {
        get: function () {
            return this === this.rootService ?
                    this.superForGet("isOffline")() :
                        this.rootService.isOffline;
        }
    },

    /**
     * Called with all the data passed to
     * [addRawData()]{@link RawDataService#addRawData} to allow storing of that
     * data for offline use.
     *
     * The default implementation does nothing. This is appropriate for
     * subclasses that do not support offline operation or which operate the
     * same way when offline as when online.
     *
     * Other subclasses may override this method to store data fetched when
     * online so [fetchData]{@link RawDataSource#fetchData} can use that data
     * when offline.
     *
     * @method
     * @argument {Object} records  - An array of objects whose properties' values
     *                               hold the raw data.
     * @argument {?DataQuery} selector
     *                             - Describes how the raw data was selected.
     * @argument {?} context       - The value that was passed in to the
     *                               [rawDataDone()]{@link RawDataService#rawDataDone}
     *                               call that invoked this method.
     * @returns {external:Promise} - A promise fulfilled when the raw data has
     * been saved. The promise's fulfillment value is not significant and will
     * usually be `null`.
     */
    writeOfflineData: {
        value: function (records, selector, context) {
            // Subclasses should override this to do something useful.
            return this.nullPromise;
        }
    },

    /***************************************************************************
     * Collecting Raw Data
     */

    /**
     * To be called by [fetchData()]{@link RawDataService#fetchData} or
     * [fetchRawData()]{@link RawDataService#fetchRawData} when raw data records
     * are received. This method should never be called outside of those
     * methods.
     *
     * This method creates and registers the data objects that
     * will represent the raw records with repeated calls to
     * [getDataObject()]{@link DataService#getDataObject}, maps
     * the raw data to those objects with repeated calls to
     * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject},
     * and then adds those objects to the specified stream.
     *
     * Subclasses should not override this method and instead override their
     * [getDataObject()]{@link DataService#getDataObject} method, their
     * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject} method,
     * their [mapping]{@link RawDataService#mapping}'s
     * [mapRawDataToObject()]{@link RawDataMapping#mapRawDataToObject} method,
     * or several of these.
     *
     * @method
     * @argument {DataStream} stream
     *                           - The stream to which the data objects created
     *                             from the raw data should be added.
     * @argument {Array} records - An array of objects whose properties'
     *                             values hold the raw data. This array
     *                             will be modified by this method.
     * @argument {?} context     - An arbitrary value that will be passed to
     *                             [getDataObject()]{@link RawDataService#getDataObject}
     *                             and
     *                             [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject}
     *                             if it is provided.
     */
    addRawData: {
        value: function (stream, records, context) {
            var offline, i, n,
                streamSelectorType = stream.query.type,
                iRecord;
            // Record fetched raw data for offline use if appropriate.
            offline = records && !this.isOffline && this._streamRawData.get(stream);
            if (offline) {
                offline.push.apply(offline, records);
            } else if (records && !this.isOffline) {
                this._streamRawData.set(stream, records.slice());
            }
            // Convert the raw data to appropriate data objects. The conversion
            // will be done in place to avoid creating any unnecessary array.
            for (i = 0, n = records && records.length; i < n; i += 1) {
                iRecord = records[i];
                /*jshint -W083*/
                // Turning off jshint's function within loop warning because the
                // only "outer scoped variable" we're accessing here is stream,
                // which is a constant reference and won't cause unexpected
                // behavior due to iteration.
                this.addOneRawData(stream, iRecord, context, streamSelectorType).then(function (mappedObject) {
                    stream.addData([mappedObject]);
                });
                /*jshint +W083*/
            }
        }
    },


    addOneRawData: {
        value: function (stream, rawData, context) {
            var object = this.objectForTypeRawData(stream.query.type, rawData, context),
                result;

            result = this._mapRawDataToObject(rawData, object, context);

            if (result && result instanceof Promise) {
                result = result.then(function () {
                    return object;
                });
            } else {
                result = Promise.resolve(object);
            }
            this._addMapDataPromiseForStream(result, stream);

            if (object) {
                this.callDelegateMethod("rawDataServiceDidAddOneRawData", this, stream, rawData, object);
            }
            return result;
        }
    },

    _addMapDataPromiseForStream: {
        value: function (promise, stream) {
            if (!this._streamMapDataPromises.has(stream)) {
                this._streamMapDataPromises.set(stream, [promise]);
            } else {
                this._streamMapDataPromises.get(stream).push(promise);
            }
        }
    },

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

    objectForTypeRawData: {
        value:function(type, rawData, context) {
                var dataIdentifier = this.dataIdentifierForTypeRawData(type,rawData);
                //Record snapshot before we may create an object
                this.recordSnapshot(dataIdentifier, rawData);
               //iDataIdentifier argument should be all we need later on
                return this.getDataObject(type, rawData, context, dataIdentifier);
        }
    },

    _typeIdentifierMap: {
        value: undefined
    },

    //This should belong on the
    //Gives us an indirection layer to deal with backward compatibility.
    dataIdentifierForTypeRawData: {
        value: function (type, rawData) {

            var mapping = this.mappingWithType(type),
                rawDataPrimaryKeys = mapping ? mapping.rawDataPrimaryKeys : null,
                rawDataPrimaryKeysValues,
                dataIdentifier, dataIdentifierMap, primaryKey;
            if(rawDataPrimaryKeys && rawDataPrimaryKeys.length) {

                dataIdentifierMap = this._typeIdentifierMap.get(type);

                if(!dataIdentifierMap) {
                    this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map()));
                }

                for(var i=0, iKey;(iKey = rawDataPrimaryKeys[i]);i++) {
                    rawDataPrimaryKeysValues = rawDataPrimaryKeysValues || [];
                    rawDataPrimaryKeysValues[i] = rawData[iKey];
                }
                if(rawDataPrimaryKeysValues) {
                    primaryKey = rawDataPrimaryKeysValues.join("/");
                    dataIdentifier = dataIdentifierMap.get(primaryKey);
                }

                if(!dataIdentifier) {
                    var typeName = type.typeName /*DataDescriptor*/ || type.name;
                        //This should be done by ObjectDescriptor/blueprint using primaryProperties
                        //and extract the corresponsing values from rawData
                        //For now we know here that MileZero objects have an "id" attribute.
                        dataIdentifier = new DataIdentifier();
                        dataIdentifier.objectDescriptor = type;
                        dataIdentifier.dataService = this;
                        dataIdentifier.typeName = type.name;
                        dataIdentifier._identifier = dataIdentifier.primaryKey = primaryKey;

                        dataIdentifierMap.set(primaryKey,dataIdentifier);
                }
                return dataIdentifier;
            }
            return undefined;
        }
    },

    __snapshot: {
        value: null
    },

    _snapshot: {
        get: function() {
            return this.__snapshot || (this.__snapshot = new Map());
        }
    },


    /**
     * Records the snapshot of the values of record known for a DataIdentifier
     *
     * @private
     * @argument  {DataIdentifier} dataIdentifier
     * @argument  {Object} rawData
     */
    recordSnapshot: {
        value: function (dataIdentifier, rawData) {
            this._snapshot.set(dataIdentifier, rawData);
        }
    },

    /**
     * Removes the snapshot of the values of record for the DataIdentifier argument
     *
     * @private
     * @argument {DataIdentifier} dataIdentifier
     */
   removeSnapshot: {
        value: function (dataIdentifier) {
            this._snapshot.delete(dataIdentifier);
        }
    },

    /**
     * Returns the snapshot associated with the DataIdentifier argument if available
     *
     * @private
     * @argument {DataIdentifier} dataIdentifier
     */
   snapshotForDataIdentifier: {
        value: function (dataIdentifier) {
            return this._snapshot.get(dataIdentifier);
       }
    },

    /**
     * Returns the snapshot associated with the DataIdentifier argument if available
     *
     * @private
     * @argument {DataIdentifier} dataIdentifier
     */
   snapshotForObject: {
        value: function (object) {
            return this.snapshotForDataIdentifier(this.dataIdentifierForObject(object));
        }
    },

    /**
     * To be called once for each [fetchData()]{@link RawDataService#fetchData}
     * or [fetchRawData()]{@link RawDataService#fetchRawData} call received to
     * indicate that all the raw data meant for the specified stream has been
     * added to that stream.
     *
     * Subclasses should not override this method.
     *
     * @method
     * @argument {DataStream} stream - The stream to which the data objects
     *                                 corresponding to the raw data have been
     *                                 added.
     * @argument {?} context         - An arbitrary value that will be passed to
     *                                 [writeOfflineData()]{@link RawDataService#writeOfflineData}
     *                                 if it is provided.
     */
    rawDataDone: {
        value: function (stream, context) {
            var self = this,
                dataToPersist = this._streamRawData.get(stream),
                mappingPromises = this._streamMapDataPromises.get(stream),
                dataReadyPromise = mappingPromises ? Promise.all(mappingPromises) : this.nullPromise;

            if (mappingPromises) {
                this._streamMapDataPromises.delete(stream);
            }

            if (dataToPersist) {
                this._streamRawData.delete(stream);
            }

            dataReadyPromise.then(function (results) {

                return dataToPersist ? self.writeOfflineData(dataToPersist, stream.query, context) : null;
            }).then(function () {
                stream.dataDone();
                return null;
            }).catch(function (e) {
                console.error(e);
            });

        }
    },

    /**
     * Records in the process of being written to streams (after
     * [addRawData()]{@link RawDataService#addRawData} has been called and
     * before [rawDataDone()]{@link RawDataService#rawDataDone} is called for
     * any given stream). This is used to collect raw data that needs to be
     * stored for offline use.
     *
     * @private
     * @type {Object.<Stream, records>}
     */
    _streamRawData: {
        get: function () {
            if (!this.__streamRawData) {
                this.__streamRawData = new WeakMap();
            }
            return this.__streamRawData;
        }
    },

    __streamRawData: {
        value: undefined
    },

    /***************************************************************************
     * Mapping Raw Data
     */

    /**
     * Convert a selector for data objects to a selector for raw data.
     *
     * The selector returned by this method will be the selector used by methods
     * that deal with raw data, like
     * [fetchRawData()]{@link RawDataService#fetchRawData]},
     * [addRawData()]{@link RawDataService#addRawData]},
     * [rawDataDone()]{@link RawDataService#rawDataDone]}, and
     * [writeOfflineData()]{@link RawDataService#writeOfflineData]}. Any
     * [stream]{@link DataStream} available to these methods will have their
     * selector references temporarly replaced by references to the mapped
     * selector returned by this method.
     *
     * The default implementation of this method returns the passed in selector.
     *
     * @method
     * @argument {DataQuery} selector - A selector defining data objects to
     *                                     select.
     * @returns {DataQuery} - A selector defining raw data to select.
     */
    mapSelectorToRawDataQuery: {
        value: function (query) {
            return query;
        }
    },

    mapSelectorToRawDataSelector: {
        value: deprecate.deprecateMethod(void 0, function (selector) {
            return this.mapSelectorToRawDataQuery(selector);
        }, "mapSelectorToRawDataSelector", "mapSelectorToRawDataQuery"),
    },

    /**
     * Convert raw data to data objects of an appropriate type.
     *
     *
     * @todo Make this method overridable by type name with methods like
     * `mapRawDataToHazard()` and `mapRawDataToProduct()`.
     *
     * @method
     * @argument {Object} record - An object whose properties' values hold
     *                             the raw data.
     * @argument {Object} object - An object whose properties must be set or
     *                             modified to represent the raw data.
     * @argument {?} context     - The value that was passed in to the
     *                             [addRawData()]{@link RawDataService#addRawData}
     *                             call that invoked this method.
     */
    mappingForObject: {
        value: function (object) {
            var objectDescriptor = this.objectDescriptorForObject(object),
                mapping = objectDescriptor && this.mappingWithType(objectDescriptor);

            if (!mapping && objectDescriptor) {
                mapping = this._objectDescriptorMappings.get(objectDescriptor);
                if (!mapping) {
                    mapping = DataMapping.withObjectDescriptor(objectDescriptor);
                    this._objectDescriptorMappings.set(objectDescriptor, mapping);
                }
            }

            return mapping;
        }
    },

    /**
     * Convert raw data to data objects of an appropriate type.
     *
     * Subclasses should override this method to map properties of the raw data
     * to data objects:
     * @method
     * @argument {Object} record - An object whose properties' values hold
     *                             the raw data.
     * @argument {Object} object - An object whose properties must be set or
     *                             modified to represent the raw data.
     * @argument {?} context     - The value that was passed in to the
     *                             [addRawData()]{@link RawDataService#addRawData}
     *                             call that invoked this method.
     */


    mapRawDataToObject: {
        value: function (rawData, object, context) {
            // return this.mapFromRawData(object, record, context);
        }
    },
    /**
     * Convert raw data to data objects of an appropriate type.
     *
     * Subclasses should override this method to map properties of the raw data
     * to data objects, as in the following:
     *
     *     mapRawDataToObject: {
     *         value: function (object, record) {
     *             object.firstName = record.GIVEN_NAME;
     *             object.lastName = record.FAMILY_NAME;
     *         }
     *     }
     *
     * Alternatively, subclasses can define a
     * [mapping]{@link DataService#mapping} to do this mapping.
     *
     * The default implementation of this method uses the service's mapping if
     * the service has one, and otherwise calls the deprecated
     * [mapFromRawData()]{@link RawDataService#mapFromRawData}, whose default
     * implementation does nothing.
     *
     * @todo Make this method overridable by type name with methods like
     * `mapRawDataToHazard()` and `mapRawDataToProduct()`.
     *
     * @method
     * @argument {Object} record - An object whose properties' values hold
     *                             the raw data.
     * @argument {Object} object - An object whose properties must be set or
     *                             modified to represent the raw data.
     * @argument {?} context     - The value that was passed in to the
     *                             [addRawData()]{@link RawDataService#addRawData}
     *                             call that invoked this method.
     */
    _mapRawDataToObject: {
        value: function (record, object, context) {
            var self = this,
                mapping = this.mappingForObject(object),
                result;

            if (mapping) {
                result = mapping.mapRawDataToObject(record, object, context);
                if (result) {
                    result = result.then(function () {
                        return self.mapRawDataToObject(record, object, context);
                    });
                } else {
                    result = this.mapRawDataToObject(record, object, context);
                }
            } else {
                result = this.mapRawDataToObject(record, object, context);
            }

            return result;

        }
    },

    /**
     * Public method invoked by the framework during the conversion from
     * an object to a raw data.
     * Designed to be overriden by concrete RawDataServices to allow fine-graine control
     * when needed, beyond transformations offered by an ObjectDescriptorDataMapping or
     * an ExpressionDataMapping
     *
     * @method
     * @argument {Object} object - An object whose properties must be set or
     *                             modified to represent the raw data.
     * @argument {Object} record - An object whose properties' values hold
     *                             the raw data.
     * @argument {?} context     - The value that was passed in to the
     *                             [addRawData()]{@link RawDataService#addRawData}
     *                             call that invoked this method.
     */
    mapObjectToRawData: {
        value: function (object, record, context) {
            // this.mapToRawData(object, record, context);
        }
    },

    /**
     * @todo Document.
     * @todo Make this method overridable by type name with methods like
     * `mapHazardToRawData()` and `mapProductToRawData()`.
     *
     * @method
     */
    _mapObjectToRawData: {
        value: function (object, record, context) {
            var mapping = this.mappingForObject(object),
                result;

            if (mapping) {
                result = mapping.mapObjectToRawData(object, record, context);
            }

            if (record) {
                if (result) {
                    var otherResult = this.mapObjectToRawData(object, record, context);
                    if (result instanceof Promise && otherResult instanceof Promise) {
                        result = Promise.all([result, otherResult]);
                    } else if (otherResult instanceof Promise) {
                        result = otherResult;
                    }
                } else {
                    result = this.mapObjectToRawData(object, record, context);
                }
            }

            return result;
        }
    },

    // /**
    //  * If defined, used by
    //  * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject} and
    //  * [mapObjectToRawData()]{@link RawDataService#mapObjectToRawData} to map
    //  * between the raw data on which this service is based and the typed data
    //  * objects which this service provides and manages.
    //  *
    //  * @type {?DataMapping}
    //  */
    // mapping: {
    //     value: undefined
    // },

    _mappingsPromise: {
        get: function () {
            if (!this.__mappingsPromise) {
                this.__mappingsPromise = Promise.all(this.mappings.map(function (mapping) {
                    return mapping.objectDescriptor;
                })).then(function (values) {

                });
            }
            return this.__mappingsPromise;
        }
    },

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

    /***************************************************************************
     * Deprecated
     */

    /**
     * @todo Document deprecation in favor of
     * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject}
     *
     * @deprecated
     * @method
     */
    mapFromRawData: {
        value: function (object, record, context) {
            // Implemented by subclasses.
        }
    },

    /**
     * @todo Document deprecation in favor of
     * [mapObjectToRawData()]{@link RawDataService#mapObjectToRawData}
     *
     * @deprecated
     * @method
     */
    mapToRawData: {
        value: function (object, record) {
            // Implemented by subclasses.
        }
    },

    /**
     * @todo Remove any dependency and delete.
     *
     * @deprecated
     * @type {OfflineService}
     */
    offlineService: {
        value: undefined
    }

});