From db59281a31a1629eb1f6cd207e45cd3cf348cbc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Cantera?= Date: Wed, 11 Jun 2014 17:22:11 +0200 Subject: [PATCH] Bug 989927 - [Contacts][Refactor] Create a library that reads the Global Contacts DataStore and provide a single contact --- .../contacts/test/unit/multi_contact_test.js | 148 ++++++++++++++ shared/js/contacts/contacts_merger.js | 76 +++++--- shared/js/contacts/multi_contact.js | 181 ++++++++++++++++++ .../unit/mocks/mock_navigator_datastore.js | 23 ++- 4 files changed, 396 insertions(+), 32 deletions(-) create mode 100644 apps/communications/contacts/test/unit/multi_contact_test.js create mode 100644 shared/js/contacts/multi_contact.js diff --git a/apps/communications/contacts/test/unit/multi_contact_test.js b/apps/communications/contacts/test/unit/multi_contact_test.js new file mode 100644 index 000000000000..8a1edffd0df5 --- /dev/null +++ b/apps/communications/contacts/test/unit/multi_contact_test.js @@ -0,0 +1,148 @@ +'use strict'; + +/* globals MockDatastoreObj, MockNavigatorDatastore, MockMozContactsObj */ +/* globals MultiContact */ + +require('/shared/js/lazy_loader.js'); +require('/shared/js/simple_phone_matcher.js'); +require('/shared/js/contacts/multi_contact.js'); +require('/shared/test/unit/mocks/mock_navigator_datastore.js'); +requireApp('communications/contacts/test/unit/mock_mozContacts.js'); + +mocha.globals(['contacts']); + + +suite('Getting MultiContact Data', function() { + + var datastore1, datastore2; + + var EXAMPLE1_APP = 'app://example.a1.org'; + var EXAMPLE2_APP = 'app://example.a2.org'; + + var CONTACTS_APP = 'app://communications.gaiamobile.org'; + + var globalEntryId = '9876'; + var ds1Id = '1234', ds2Id = '4567'; + + // Global entry on the GCDS references two different datastores + var entry = { + id: globalEntryId, + entryData: [ + { + origin: EXAMPLE1_APP, + uid: ds1Id + }, + { + origin: EXAMPLE2_APP, + uid: ds2Id + } + ] + }; + + var entryMozContacts = { + id: globalEntryId, + entryData: [ + { + origin: EXAMPLE1_APP, + uid: ds1Id + }, + { + origin: CONTACTS_APP, + uid: 'abcdef' + } + ] + }; + + var ds1Records = Object.create(null); + ds1Records[ds1Id] = { + id: ds1Id, + givenName: ['Jose'], + familyName: null, + tel: [ + { + type: ['work'], + value: '983367741' + } + ] + }; + + var ds2Records = Object.create(null); + ds2Records[ds2Id] = { + id: ds2Id, + familyName: ['Cantera'], + email: [ + { + type: ['personal'], + value: 'jj@jj.com' + } + ] + }; + + var aMozTestContact = { + id: 'abcdef', + givenName: ['Carlos'], + familyName: ['Fernández'], + tel: [ + { + type: ['home'], + value: '638883076' + } + ] + }; + + var realDatastore, realMozContacts; + + suiteSetup(function() { + datastore1 = new MockDatastoreObj('contacts', EXAMPLE1_APP, ds1Records); + datastore2 = new MockDatastoreObj('contacts', EXAMPLE2_APP, ds2Records); + + MockNavigatorDatastore._datastores = [ + datastore1, + datastore2 + ]; + + realDatastore = navigator.getDataStores; + realMozContacts = navigator.mozContacts; + + navigator.getDataStores = MockNavigatorDatastore.getDataStores; + navigator.mozContacts = new MockMozContactsObj([aMozTestContact]); + }); + + suiteTeardown(function() { + navigator.getDataStores = realDatastore; + navigator.mozContacts = realMozContacts; + }); + + test('Getting data from two different datastores', function(done) { + MultiContact.getData(entry).then(function success(data) { + done(function() { + assert.equal(data.id, globalEntryId); + + assert.equal(data.familyName[0], 'Cantera'); + assert.equal(data.givenName[0], 'Jose'); + assert.equal(data.tel.length, 1); + assert.equal(data.email.length, 1); + }); + }, function error(err) { + done(function() { + assert.fail('Error while getting data'); + }); + }); + }); + + test('Getting data from a datastore and mozContacts', function(done) { + MultiContact.getData(entryMozContacts).then(function success(data) { + done(function() { + assert.equal(data.id, globalEntryId); + + assert.equal(data.familyName[0], 'Fernández'); + assert.equal(data.givenName[0], 'Carlos'); + assert.equal(data.tel.length, 2); + }); + }, function error(err) { + done(function() { + assert.fail('Error while getting data'); + }); + }); + }); +}); diff --git a/shared/js/contacts/contacts_merger.js b/shared/js/contacts/contacts_merger.js index 626b9a304f4a..02f20df8cdde 100644 --- a/shared/js/contacts/contacts_merger.js +++ b/shared/js/contacts/contacts_merger.js @@ -1,4 +1,4 @@ -/* globals SimplePhoneMatcher, utils, ContactPhotoHelper */ +/* globals Promise, SimplePhoneMatcher, utils, ContactPhotoHelper */ 'use strict'; @@ -41,16 +41,33 @@ contacts.Merger = (function() { // from an external source not related with the matching algorithm. function doMerge(pmasterContact, pmatchingContacts, callbacks) { window.setTimeout(function contactsMerge() { - mergeAll(pmasterContact, pmatchingContacts, callbacks); + mergeAll(pmasterContact, pmatchingContacts, filled, callbacks); }, 0); } + + function doInMemoryMerge(pmasterContact, pmatchingContacts) { + return new Promise(function (resolve, reject) { + mergeAll(pmasterContact, pmatchingContacts, inMemoryMergeDone, { + success: function(masterContact) { + resolve(masterContact); + }, + error: function(err) { + reject(err); + } + }); + }); + } + + function inMemoryMergeDone(callbacks, matchingContacts, masterContact) { + callbacks.success(masterContact); + } function isSimContact(contact) { return Array.isArray(contact.category) && contact.category.indexOf('sim') !== -1; } - function mergeAll(masterContact, matchingContacts, callbacks) { + function mergeAll(masterContact, matchingContacts, onFilled, callbacks) { var emailsHash; var categoriesHash; var telsHash; @@ -228,32 +245,34 @@ contacts.Merger = (function() { mergedContact.familyName[0] : '')).trim()]; fillMasterContact(masterContact, mergedContact, mergedPhoto, - function filled(masterContact) { - // Updating the master contact - var req = navigator.mozContacts.save( - utils.misc.toMozContact(masterContact)); - - req.onsuccess = function() { - // Now for all the matchingContacts they have to be removed - matchingContacts.forEach(function(aMatchingContact) { - // Only remove those contacts which are already in the DB - if (aMatchingContact.matchingContact.id) { - var contact = aMatchingContact.matchingContact; - navigator.mozContacts.remove(utils.misc.toMozContact(contact)); - } - }); - - if (typeof callbacks.success === 'function') { - callbacks.success(masterContact); + onFilled.bind(null, callbacks, matchingContacts)); + } + + function filled(callbacks, matchingContacts, masterContact) { + // Updating the master contact + var req = navigator.mozContacts.save( + utils.misc.toMozContact(masterContact)); + + req.onsuccess = function() { + // Now for all the matchingContacts they have to be removed + matchingContacts.forEach(function(aMatchingContact) { + // Only remove those contacts which are already in the DB + if (aMatchingContact.matchingContact.id) { + var contact = aMatchingContact.matchingContact; + navigator.mozContacts.remove(utils.misc.toMozContact(contact)); } - }; + }); - req.onerror = function() { - window.console.error('Error while saving merged Contact: ', - req.error.name); - typeof callbacks.error === 'function' && callbacks.error(req.error); - }; - }); + if (typeof callbacks.success === 'function') { + callbacks.success(masterContact); + } + }; + + req.onerror = function() { + window.console.error('Error while saving merged Contact: ', + req.error.name); + typeof callbacks.error === 'function' && callbacks.error(req.error); + }; } function isDefined(field) { @@ -352,7 +371,8 @@ contacts.Merger = (function() { } return { - merge: doMerge + merge: doMerge, + inMemoryMerge: doInMemoryMerge }; })(); diff --git a/shared/js/contacts/multi_contact.js b/shared/js/contacts/multi_contact.js new file mode 100644 index 000000000000..7fa94d443df6 --- /dev/null +++ b/shared/js/contacts/multi_contact.js @@ -0,0 +1,181 @@ +'use strict'; + +/* exported MultiContact */ +/* globals Promise, LazyLoader, contacts */ + +// ATTENTION: This library lazy loads contacts_merger.js + +var MultiContact = (function() { + var datastores = Object.create(null); + var datastoresLoading = false; + var DS_READY_EVENT = 'ds_ready'; + + var MOZ_CONTACTS_OWNER = 'app://communications.gaiamobile.org'; + + function getDatastore(owner) { + if (datastores[owner]) { + return Promise.resolve(datastores[owner]); + } + + return new Promise(function(resolve, reject) { + if (datastoresLoading === true) { + document.addEventListener(DS_READY_EVENT, function handler() { + document.removeEventListener(DS_READY_EVENT, handler); + resolve(datastores[owner]); + }); + } + else { + datastoresLoading = true; + + navigator.getDataStores('contacts').then(function success(dsList) { + dsList.forEach(function(aDs) { + datastores[aDs.owner] = aDs; + }); + // This is needed because mozContact DB is not exposed as a Datastore + // Once bug 1016838 lands this will not be needed + datastores[MOZ_CONTACTS_OWNER] = new MozContactsDatastore(); + resolve(datastores[owner]); + datastoresLoading = false; + document.dispatchEvent(new CustomEvent(DS_READY_EVENT)); + }, function err(error) { + console.error('Error while obtaining datastores: ', error.name); + }); + } + }); + } + + // Adapter object to obtain data from the mozContacts as if it were a DS + // Once bug 1016838 lands this will not be needed + function MozContactsDatastore() { + } + + MozContactsDatastore.prototype = { + get: function(id) { + return new Promise(function(resolve, reject) { + + var options = { + filterBy: ['id'], + filterOp: 'equals', + filterValue: id + }; + + var req = navigator.mozContacts.find(options); + + req.onsuccess = function() { + resolve(JSON.parse(JSON.stringify(req.result[0]))); + }; + + req.onerror = function() { + reject(req.error); + }; + }); + }, + get name() { + return 'mozContacts'; + } + }; + + function getData(entry) { + if (!entry || !entry.id || !Array.isArray(entry.entryData)) { + return Promise.reject({ + name: 'InvalidEntry' + }); + } + + return new Promise(function(resolve, reject) { + var operations = []; + + var entryData = entry.entryData; + + var mozContactId; + entryData.forEach(function fetchEntry(aEntry) { + var owner = aEntry.origin; + if (owner === MOZ_CONTACTS_OWNER) { + mozContactId = aEntry.uid; + } + + getDatastore(owner).then(function success(datastore) { + operations.push(datastore.get(aEntry.uid)); + // It is needed to wait to have all operations ready + if (operations.length === entryData.length) { + execute(operations, resolve, reject, { + targetId: entry.id, + mozContactId: mozContactId + }); + } + }, function error(err) { + console.error('Error while obtaining datastore: ', err.name); + reject(err); + }); + }); + }); + } + + + function execute(operations, resolve, reject, options) { + Promise.all(operations).then(function success(results) { + if (results.length === 1) { + resolve(results[0]); + return; + } + + if (options.mozContactId) { + results = reorderResults(results, options.mozContactId); + } + + LazyLoader.load('/shared/js/contacts/contacts_merger.js', + function() { + var matchings = createMatchingContacts(results); + + contacts.Merger.inMemoryMerge(results[0], matchings).then( + function success(mergedResult) { + mergedResult.id = options.targetId; + resolve(mergedResult); + }, function error(err) { + console.log('Error while merging: ', err); + reject(err); + }); + }); + }, function error(err) { + console.error('Error while getting data: ', err.name); + reject(err); + }); + } + + + // This function reorders the contact data results array in order to + // put the mozContact in the first position, as mozContact data will be taken + // precedence over other datastore data + function reorderResults(results, mozContactId) { + var out = []; + + for (var j = 0; j < results.length; j++) { + if (results[j].id === mozContactId) { + out.unshift(results[j]); + } + else { + out.push(results[j]); + } + } + + return out; + } + + // This function adapts the result array to the input object expected by + // the contacts_merger module + function createMatchingContacts(results) { + var out = []; + + for (var j = 1; j < results.length; j++) { + out.push({ + matchingContact: results[j] + }); + } + + return out; + } + + return { + 'getData': getData + }; +})(); diff --git a/shared/test/unit/mocks/mock_navigator_datastore.js b/shared/test/unit/mocks/mock_navigator_datastore.js index 221c0d7a58e3..68ea145aa003 100644 --- a/shared/test/unit/mocks/mock_navigator_datastore.js +++ b/shared/test/unit/mocks/mock_navigator_datastore.js @@ -1,11 +1,17 @@ 'use strict'; -var MockDatastore = { +/* exports MockDatastore, MockDatastoreObj */ + +function MockDatastoreObj(name, owner, records) { + this.name = name || 'Mock_Datastore'; + this.owner = owner; + this._records = records || Object.create(null); +} + +MockDatastoreObj.prototype = { readOnly: false, revisionId: '123456', - name: 'Mock_Datastore', - _records: Object.create(null), _nextId: 1, _inError: false, _cb: null, @@ -115,7 +121,11 @@ var MockDatastore = { } }; +var MockDatastore = new MockDatastoreObj(); + var MockNavigatorDatastore = { + _datastores: null, + getDataStores: function() { if (MockNavigatorDatastore._notFound === true) { return new window.Promise(function(resolve, reject) { @@ -124,7 +134,12 @@ var MockNavigatorDatastore = { } return new window.Promise(function(resolve, reject) { - resolve([MockDatastore]); + if (!MockNavigatorDatastore._datastores) { + resolve([MockDatastore]); + } + else { + resolve(MockNavigatorDatastore._datastores); + } }); } };