js/base/services/SyncAPIService.js
'use strict';
// TODO all the rest calls should move to SynthesisRESTClient
var SyncAPIService = ($q, $http, $filter, $rootScope, base64, DataService,
LoggerService, SynthError, _SF, CheckError,
SynthConfig, ModuleService, RegistrationService, SynthAttachmentMiner,
SynthEmbeddedImageHandler, SynthDeleteHandler, SynthLinkHandler, SynthUploadResponseHandler) => {
var LOG = LoggerService('SyncAPIService');
function mapTool(toolname){
//LOG.debug('Getting mapped name for : ' + toolname + ' : ' + TOOLMAP[toolname] || toolname);
return SynthConfig.toolMapping[toolname] || toolname;
}
/**
* Constructor
*/
function SyncAPIServiceImpl(){
}
/**
* Get the offline sync status for a specific tool
* Resolves with an object:
*
* {
'toolId' : 'resources',
'moduleId' : 'biology',
'contentUploadSize' : 1024,
'contentDownloadSize' : 1024,
'total' : 0,
'inSync': false,
'label': 'Announcement'
}
*/
SyncAPIServiceImpl.prototype.getSyncStatusToolOffline = function(moduleId, toolId){
var self = this,
syncStatus = {
'toolId' : toolId,
'moduleId' : moduleId,
'contentUploadSize' : 0,
'contentDownloadSize' : 0,
'total' : 0,
'inSync' : true,
'label' : null
};
return ModuleService.getModuleData(moduleId).then((moduleData) => {
var toolLocal = moduleData.toolsLocal[toolId];
var toolRemote = moduleData.tools[toolId];
syncStatus.label = moduleData.toolDescriptions[toolId].label;
// If there is no remote entry for the tool, we don't know if its out of sync. Lets assume it's out
if (toolRemote == null){
syncStatus.inSync = false;
syncStatus.contentDownloadSize = 0; // We actually don't know the size
}
// Check for content download
else if(toolLocal.clientContentVersion !== toolRemote.currentContentVersion){
syncStatus.contentDownloadSize = toolRemote.contentSynchSize;
syncStatus.inSync = false;
}
// Check for local change
return self.getToolUploadSize(moduleId, toolId).then((size) => {
if(size !== 0){
syncStatus.contentUploadSize = size;
syncStatus.inSync = false;
}
// Add up the totals for download and upload
syncStatus.total = syncStatus.contentDownloadSize + syncStatus.contentUploadSize;
return syncStatus;
});
});
};
/**
* Get the offline sync status for a ALL modules without going online
* Resolves with an object:
*
* {
'inSync' : true,
'contentDownloadSize': 0,
'contentUploadSize': 0,
'total': 0,
'modules': {
'biology' : {
'inSync' : true,
'contentDownloadSize': 0,
'contentUploadSize': 0,
'total': 0,
'tools' : {
'resources' : {
'toolId' : 'resources',
'moduleId' : 'biology',
'contentDownloadSize' : 1024,
'contentUploadSize' : 1024,
'inSync': false,
'label': 'Announcement'
}
}
}
}
}
*/
SyncAPIServiceImpl.prototype.getSyncStatusModulesOffline = function(){
var self = this,
syncStatus = {
'inSync' : true,
'contentDownloadSize' : 0,
'contentUploadSize' : 0,
'total' : 0,
'modules' : {}
};
/**
* Returns a promise to loop through all the modules to get their sync status
*/
function getLoopModulesPromise(linkedModules){
let promise = $q.when();
angular.forEach(linkedModules, function(linkedModule){
promise = promise.then(function(){
return self.getSyncStatusModuleOffline(linkedModule.id)
.then((moduleSyncStatus) => {
// Add the module to the modules map
syncStatus.modules[linkedModule.id] = moduleSyncStatus;
// Update the overall sync status to include the totals of the module
syncStatus.inSync &= moduleSyncStatus.inSync;
syncStatus.contentDownloadSize += moduleSyncStatus.contentDownloadSize;
syncStatus.contentUploadSize += moduleSyncStatus.contentUploadSize;
syncStatus.total += moduleSyncStatus.total;
return syncStatus;
});
});
});
return promise.then(() => {
return syncStatus;
});
}
return ModuleService.getLinkedModules()
.then(getLoopModulesPromise);
};
/**
* Get the offline sync status for a specific module without going online
* Resolves with an object:
*
* {
* 'inSync' : true,
'contentDownloadSize': 0,
'contentUploadSize': 0,
'total': 0,
'moduleId' : 'biology',
'tools' : {
'resources' : {
'toolId' : 'resources',
'moduleId' : 'biology',
'contentUploadSize' : 1024,
'contentDownloadSize' : 1024,
'isDownload' : true,
'label': 'Announcement'
}
}
}
*/
SyncAPIServiceImpl.prototype.getSyncStatusModuleOffline = function(moduleId){
var self = this;
// The response object that will be sent to the caller of this function
var syncStatus = {
'inSync' : true,
'download' : 0,
'upload' : 0,
'total' : 0,
'moduleId' : moduleId,
'tools' : {}
};
var funcUpdateTool = function(toolId){
return self.getSyncStatusToolOffline(moduleId, toolId).then((toolSyncStatus) => {
// Add the tool's sync status to the module's map of tools
syncStatus.tools[toolId] = toolSyncStatus;
// Update the module sync status to include the totals of the tools
syncStatus.inSync &= toolSyncStatus.inSync;
syncStatus.download += toolSyncStatus.download;
syncStatus.upload += toolSyncStatus.upload;
syncStatus.total += toolSyncStatus.total;
});
};
return ModuleService.getModuleData(moduleId).then((moduleData) => {
// If we don't have tool we must be out of sync
if(moduleData == null || moduleData.tools == null){
syncStatus.inSync = false;
return syncStatus; // Resolve with this sync status
}
else{
let toolsArray = $filter('object2Array')(moduleData.toolsLocal);
let promise = $q.when();
angular.forEach(toolsArray, function(tool){
promise = promise.then(function(){
return funcUpdateTool(tool.key);
});
});
// Else return another promise
return promise.then(function(){
return syncStatus;
});
}
});
};
/**
* Get the module upload size and save it to module.json
*/
SyncAPIServiceImpl.prototype.getToolUploadSize = function(moduleId, toolId){
// Get promise to merge tool upload size to module.json
function getMergeModuleDataPromise(size){
// We also need to update our local file about the possible change of upload content
var mergeData = { 'toolsLocal' : {} };
mergeData.toolsLocal[toolId] = {
'localChange' : (size !== 0),
'localChangeSize' : size
};
return ModuleService
.mergeToModuleData(moduleId, mergeData)
.then(() => {
return size;
});
}
return DataService
.getToolUploadDataSize(moduleId, toolId)
.then(getMergeModuleDataPromise);
};
/**
* Update the sync status
*/
SyncAPIServiceImpl.prototype.getSyncStatus = function(moduleId){
// The response object that will be sent to the caller of this function
var syncStatus = {
'inSync' : true,
'tools' : {}
};
if(moduleId == null){
return $q.when(syncStatus);
}
var moduleData = null, registration, self = this;
function getRegistrationData(){
return RegistrationService.getRegistration()
.then((registrationData) => {
registration = registrationData;
});
}
// Returns a promise to get the module data
function getModuleDataPromise(){
return ModuleService
.getModuleData(moduleId)
.then((mData) => {
moduleData = mData;
});
}
// Returns a promise to get the sync status from the remote server
function getRequestSyncStatusPromise(){
var deferred = $q.defer();
// Data that will be sent with the sync status request
var reqData = {
'tools' : {}
};
// Map mobile app tools to remote server tool names and add current versions
for(var key in moduleData.toolsLocal){
var mappedKey = mapTool(key);
reqData.tools[mappedKey] = {
'clientCodeVersion' : moduleData.toolsLocal[key].clientCodeVersion,
'clientContentVersion' : moduleData.toolsLocal[key].clientContentVersion
};
}
// Request changes from remote server
var restURL = SynthConfig.baseURL + '/service-synch/synchStatus/' + registration.username + '/' + moduleId;
LOG.debug('Getting sync status calling REST URL : ' + restURL);
$http({
method : 'POST',
url : restURL,
data : reqData})
.then((response) => {
// Check if there is an error
if (CheckError(deferred, response.data)){
return;
}
deferred.resolve(response.data.tools);
}, () => {
deferred.reject(SynthError(1000));
});
return deferred.promise;
}
// Returns a promise to write the response data to the module.json file
function getWriteDownloadResponsePromise(responseTools){
var fileData = { 'tools' : {}};
// Map service tools to local tool names
for(var remoteKey in responseTools){
var localKey = mapTool(remoteKey);
// Only if the tool is in our list of active tool
if(moduleData.toolDescriptions[localKey] != null){
// Data to write to file
fileData.tools[localKey] = {
'clientContentVersion' : responseTools[remoteKey].clientContentVersion,
'currentContentVersion' : (responseTools[remoteKey].currentContentVersion === 'No Content' ? '0' : responseTools[remoteKey].currentContentVersion),
'contentSynchSize' : responseTools[remoteKey].contentSynchSize
};
// Data to send back caller of this function
syncStatus.tools[localKey] = {
'label' : moduleData.toolDescriptions[localKey].label,
'contentDownloadSize' : responseTools[remoteKey].contentSynchSize,
'contentUploadSize' : 0,
'inSync' : (responseTools[remoteKey].contentSynchSize === 0) // Ignoring code sync for mobile
};
syncStatus.download = ((syncStatus.download ? syncStatus.download : 0) + responseTools[remoteKey].contentSynchSize);// Ignoring code sync for mobile
// If any tool has downloads, we are overall out of sync
if (!syncStatus.tools[localKey].inSync){
syncStatus.inSync = false;
}
}
}
return ModuleService.mergeToModuleData(moduleId, fileData);
}
/**
* Gets a promise to check the upload size for all tools
*/
function getUploadSizePromise(){
var promise = $q.when();
var toolsArray = null;
// Create the array if we don't have it
if(toolsArray == null){
toolsArray = $filter('object2Array')(syncStatus.tools);
}
angular.forEach(toolsArray, function(tool){
let toolId = tool.key;
syncStatus.tools[toolId] = syncStatus.tools[toolId] || {
'codeDownloadSize' : 0,
'contentDownloadSize' : 0,
'contentUploadSize' : 0,
'inSync' : true
};
syncStatus.tools[toolId].label = moduleData.toolDescriptions[toolId].label;
promise = promise.then(function(){
return self.getToolUploadSize(moduleId, toolId).then((size) => {
syncStatus.tools[toolId].contentUploadSize = size;
syncStatus.tools[toolId].inSync = (syncStatus.tools[toolId].inSync && (size === 0));
syncStatus.upload = ((syncStatus.upload ? syncStatus.upload : 0) + size);
syncStatus.inSync = (syncStatus.inSync && syncStatus.tools[toolId].inSync);
});
});
});
return promise;
}
return getRegistrationData()
// Get the module data
.then(getModuleDataPromise)
// Get sync status from remote server
.then(getRequestSyncStatusPromise)
// Write the response to the module file
.then(getWriteDownloadResponsePromise)
// Get the upload size
.then(getUploadSizePromise)
.then(() => {
syncStatus.total = ((syncStatus.download ? syncStatus.download : 0) + (syncStatus.upload ? syncStatus.upload : 0));
return syncStatus; // Finally we are done!
});
};
/**
* Updates a specific tool
*/
SyncAPIServiceImpl.prototype.syncDownloadTool = function(moduleId, toolname){
var moduleData = null,
self = this,
toolSyncResponse = {},
toolDataObject = {}, // The data synced for this tool
toolAttachments = [], // Attachments that has to be downloaded
registration = null;
$rootScope.$broadcast('syncStatusChanged', {'action' : 'downloading'});
function getRegistrationData(){
return RegistrationService.getRegistration()
.then((registrationData) => {
registration = registrationData;
});
}
// Returns a promise to get the module data
function getModuleDataPromise(){
return ModuleService
.getModuleData(moduleId)
.then((mData) => {
moduleData = mData;
});
}
/*
* When we update client base, we need to make sure that we only
* include the tools that was configured to be enabled on the mobile application
*/
function filterBaseTools(toolList){
var filteredData = {'toolDescriptions' : {} };
var toolDescriptions = toolList == null ? {} : toolList.toolDescriptions;
for(var serverToolId in toolDescriptions){
var localToolId = mapTool(serverToolId);
/*
* Only if the current module data contains a tool description, should the
* tool be included in the updated data.
*/
if(moduleData.toolDescriptions[localToolId] !== undefined){
filteredData.toolDescriptions[localToolId] = toolDescriptions[serverToolId];
}
}
return filteredData;
}
/*
* Returns a promise to get the tool's data
*/
function getToolDataPromise(){
var deferredData = $q.defer();
var toolVersion = moduleData.toolsLocal[toolname].clientContentVersion;
var _toolname = mapTool(toolname);
var restURL = `${SynthConfig.baseURL}/service-synch/contentUpdateString/${registration.username}/${moduleId}/${_toolname}/${toolVersion}`;
$http({
method : 'POST',
url : restURL,
data : {'authToken' : registration.authToken}})
.success((data) => {
// Check if there is an error
if (CheckError(deferredData, data)){
return;
}
toolSyncResponse = data;
deferredData.resolve(data);
})
.error(() => {
deferredData.reject(SynthError(1000));
});
return deferredData.promise;
}
/*
* When we get data from the server it is in base 64,
* this function will convert it to json format string
*/
function getConvertFromBase64Promise (base64Data){
try{
toolDataObject = base64.decode(base64Data.content);
LOG.debug(`Got data for tool ${toolname}\n${toolDataObject}`);
toolDataObject = JSON.parse(toolDataObject);
$q.when(toolDataObject);
}
catch(e){
$q.reject(SynthError(1000));
}
}
/*
* Replace the inline images with links to the downloaded content
* This function will use the registered SynthEmbeddedImageHandler for the tool if
* there was on registered
*/
function getFixImagesPromise(){
var imageHandler = SynthEmbeddedImageHandler.getHandler(toolname);
if (imageHandler){
return imageHandler(toolDataObject);
}
else{
return $q.when([]);
}
}
/*
* Replace all the embedded links with links that will open externally
*/
function getFixLinksPromise(){
var linkHandler = SynthLinkHandler.getHandler(toolname);
if (linkHandler){
return linkHandler(toolDataObject).then((fixedContent)=>{
toolDataObject = fixedContent;
});
}
return $q.when([]);
}
/*
* Mine for attachments in the tool data
*/
function getMineAttachementsPromise(){
var attachmentHandler = SynthAttachmentMiner.getHandler(toolname);
if (attachmentHandler){
return attachmentHandler(toolDataObject).then((attachments) => {
toolAttachments = attachments;
});
}
else{
return $.when([]);
}
}
/*
* Merge the tool data to the tool's data file
*/
function getMergeToolDataPromise(){
if(toolname == 'base'){
toolDataObject = filterBaseTools(toolDataObject);
return ModuleService.mergeToModuleData(moduleId, toolDataObject);
}
else{
return ModuleService.mergeToToolData(moduleId, toolname, toolDataObject);
}
}
/*
* Download the attachments
*/
function getDownloadAttachmentsPromise(){
return self.getAttachementsFromServer(toolAttachments);
}
/*
* Update the tool version
*/
function getUpdateToolVersionPromise(){
var versionData = {
'toolsLocal' : {},
'tools' : {}
};
versionData.toolsLocal[toolname] = { 'clientContentVersion' : toolSyncResponse.version };
versionData.tools[toolname] = {
'currentContentVersion' : toolSyncResponse.version,
'clientContentVersion' : toolSyncResponse.version,
'contentSynchSize' : 0
};
return ModuleService.mergeToModuleData(moduleId, versionData);
}
/*
* Returns a promise to delete all the content (including actual files)
*/
function getCheckDeletedContentPromise(toolData){
// Get the handler if there was one
var deleteHandler = SynthDeleteHandler.getHandler(toolname);
var handlerData; // Data that the handler returned after processing
if (deleteHandler){
return deleteHandler(toolData)
.then((hData) => {
handlerData = hData;
return DataService.deleteCDVFiles(handlerData.deleted);
})
// Now save the new data that the handler processed
.then(() => {
return DataService.writeToolData(moduleId, toolname, handlerData.data, false);
});
}
else{
return $.when([]);
}
}
function fail(error){
$rootScope.$broadcast('syncStatusChanged', {'action' : 'none'});
return $q.reject(error);
}
// Lets start the sync process
return getRegistrationData()
.then(getModuleDataPromise)
.then(getToolDataPromise)
.then(getConvertFromBase64Promise)
.then(getMineAttachementsPromise)
.then(getDownloadAttachmentsPromise)
.then(getFixImagesPromise)
.then(getFixLinksPromise)
.then(getMergeToolDataPromise)
.then(getCheckDeletedContentPromise)
.then(getUpdateToolVersionPromise)
.then(() => {
$rootScope.$broadcast('syncStatusChanged', {'action' : 'none'});
}, fail);
};
/**
* Updates a specific tool
*/
SyncAPIServiceImpl.prototype.syncUploadTool = function(moduleId, toolname){
var toolUploadRequest = {}, // Data to upload for the tool
toolUploadResponse = {}, // Response for the upload
registrationData = {};
// Broadcast that we are busy uploading
$rootScope.$broadcast('syncStatusChanged', {'action' : 'uploading'});
/*
* Get the registration data for the user to get
* the device ID and username
*/
var funcGetRegistrationData = function(){
return RegistrationService.getRegistration()
.then((data) => {
registrationData = data;
});
},
/*
* Get the data for the tool that should get uploaded
*/
funcGetToolUploadData = function(){
return DataService
.getToolData(moduleId, toolname, true)
.then((uploadData) => {
toolUploadRequest = uploadData;
});
},
/*
* Upload the data to the SynthEngine
*/
funcUploadToolData = function(){
var uploadDeferred = $q.defer();
var restURL = SynthConfig.baseURL + '/service-synch/content/' + registrationData.username + '/' + moduleId + '/' + mapTool(toolname);
$http({
'method' : 'PUT',
'url' : restURL,
'data' : {'authToken' : registrationData.authToken, 'content' : toolUploadRequest}
})
.success((responseData) => {
// Check if there is an error
if (CheckError(uploadDeferred, responseData)){
return;
}
toolUploadResponse = responseData.responseContent;
uploadDeferred.resolve(toolUploadResponse);
})
.error(() => {
uploadDeferred.reject(SynthError(1000));
});
return uploadDeferred.promise;
},
/*
* Delete the file containing the upload data.
*/
funcDeleteToolUploadData = function(){
return DataService.deleteToolUploadData(moduleId, toolname);
},
/*
* Merge the data we uploaded with our current data file for the tool.
* If there is a handler that needs work with the upload data and the
* upload response, we will use that before merging the data
*/
funcMergeToolData = function(){
// Check if there is an upload handler and use it
var uploadResponseHandler = SynthUploadResponseHandler.getHandler(toolname);
if(uploadResponseHandler){
toolUploadRequest = uploadResponseHandler(toolUploadRequest, toolUploadResponse);
}
return ModuleService.mergeToToolData(moduleId, toolname, toolUploadRequest, false);
};
function fail(error){
$rootScope.$broadcast('syncStatusChanged', {'action' : 'none'});
return $q.reject(error);
}
// Kick off the upload process
return funcGetRegistrationData()
.then(funcGetToolUploadData)
.then(funcUploadToolData)
.then(funcDeleteToolUploadData)
.then(funcMergeToolData)
.then(() => {
$rootScope.$broadcast('syncStatusChanged', {'action' : 'none'});
}, fail);
};
/**
* Update the Modules data file with the content from the remote server.
* This should only be called once per module, and only when the user
* registers for a module.
*
* This function will also check that the tools returned are supported by
* this client, therefor it might only use a subset of the returned data.
* The allowed tools are determined by the base.json file which is used for
* the base data file for each module (looking at the toolsLocal ids)
*
*/
SyncAPIServiceImpl.prototype.updateModuleData = function(moduleId){
var deferred = $q.defer();
var restURL = SynthConfig.baseURL + '/service-creator/tools/' + moduleId;
LOG.debug('Getting module data using REST URL : ' + restURL);
$http({
method : 'GET',
url : restURL})
.success((data) => {
if(LOG.isDEBUG()){
LOG.debug('Got response for module data : ' + JSON.stringify(data, '\t', 4));
}
// Check if there is an error
if (CheckError(deferred, data)) {
return;
}
ModuleService.getModuleData(moduleId).then(
function(moduleData){
if(LOG.isDEBUG()){
LOG.debug('Got current module data : ' + JSON.stringify(moduleData, '\t', 4));
}
var mergeData = { 'toolDescriptions' : {}, 'tools' : {}};
// Loop through the data in the response
for(var idx in data){
var toolInfo = data[idx];
var toolKeyLocal = mapTool(toolInfo.name);
// Only use the tool if it is a tool we support
if(moduleData.toolDescriptions[toolKeyLocal] !== undefined){
mergeData.toolDescriptions[toolKeyLocal] = {};
mergeData.toolDescriptions[toolKeyLocal].label = toolInfo.title;
//mergeData.toolDescriptions[toolKeyLocal].description = data.toolDescriptions[toolKey].description;
mergeData.toolDescriptions[toolKeyLocal].menu = toolInfo.onMenu;
}
}
// Now merge the resulting data to our local file
LOG.debug('We will now merge these : ' + JSON.stringify(mergeData, '\t', 4));
ModuleService.mergeToModuleData(moduleId, mergeData).then(
// Success
() => {
deferred.resolve();
},
_SF(deferred));
}, _SF(deferred));
})
.error(() => {
LOG.warn('Failed to get module data from remote server');
deferred.reject(SynthError(1000));
});
return deferred.promise;
};
/**
* Download an array of files from the remote server for a specific tool
*/
SyncAPIServiceImpl.prototype.getAttachementsFromServer = function(attachments) {
const self = this;
if (!attachments || attachments.length === 0){
LOG.info('No need to download any attachments, an empty array was given');
return $q.when({});
}
let promise = $q.when();
angular.forEach(attachments, function(attachment){
promise = promise.then(function(){
return self.getFileFromServer(attachment.downloadKey, attachment.downloadPath);
});
});
return promise;
};
/**
* Download a single file from the remote server.
*
* @param {type} downloadKey - Key of the file to download from the remote server
* @param {type} localPath - Full path to where the file should be saved locally on the device
*/
SyncAPIServiceImpl.prototype.getFileFromServer = function (downloadKey, localPath) {
var deferred = $q.defer();
// First make sure that the local path exists
var fileTransfer = new FileTransfer();
var uri = encodeURI(SynthConfig.baseURL + '/service-creator/download/file/' + downloadKey);
LOG.debug('Downloading file with URL ' + uri);
// Send back progress
fileTransfer.onprogress = function(progressEvent) {
deferred.notify(progressEvent);
};
fileTransfer.download(
uri,
localPath,
function() {
LOG.debug('Download is complete');
deferred.resolve();
},
function(error) {
LOG.warn('Failed to download file!');
LOG.warn('download error source ' + error.source);
LOG.warn('download error target ' + error.target);
LOG.warn('download error code ' + error.code);
deferred.reject(SynthError(1004));
},
false,
{
// No options
}
);
return deferred.promise;
};
return new SyncAPIServiceImpl();
};
SyncAPIService.$inject = ['$q', '$http', '$filter', '$rootScope', 'base64', 'DataService',
'LoggerService', 'SynthError', 'SynthFail', 'SynthCheckResponseError',
'SynthConfig', 'ModuleService', 'RegistrationService', 'SynthAttachmentMiner',
'SynthEmbeddedImageHandler', 'SynthDeleteHandler', 'SynthLinkHandler', 'SynthUploadResponseHandler'];
export default SyncAPIService;