/**
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
* Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
*/
/* globals app, module, __dirname */
/* jshint esversion: 6 */
var config = require('./sampleConnectorConfig'),
responses = require('./sampleConnectorResponses'),
translationProvider = require('../provider/translationProvider').factory.create(),
persistenceStore = require('../job-manager/persistenceStore').factory.create(),
jobManager = require('../job-manager/sampleJobManager');
/**
* @constructor
* @alias SampleConnector
*/
var SampleConnector = function () {};
// Internal function to validate the LSP bearer token is provided along with any required parameters
SampleConnector.prototype.validateRequest = function (req, checks) {
// Sample connector is protected by Basic Auth but needs a bearer token to communicate with the mock LSP server
// Replace this with whatever is required by the real Language Service Provider
var bearerToken = req.get('X-CEC-BearerToken') || req.get('BearerToken'),
tokenType = bearerToken ? bearerToken.split(' ')[0] : '';
if (bearerToken && (['Basic', 'Bearer'].indexOf(tokenType) === -1)) {
// add 'Bearer' to token value if not already defined
bearerToken = 'Bearer ' + bearerToken;
}
// get the arguments passed through the request
var args = {
BearerToken: bearerToken,
params: req.params,
headers: req.headers,
data: req.body
},
functionName = checks && checks.functionName || 'validateRequest',
requiresCommunity = checks && checks.hasOwnProperty('requiresCommunity') ? checks.requiresCommunity : true;
return new Promise(function (resolve, reject) {
// must have a BearerToken parameter
if (!args.BearerToken) {
reject({
errorCode: 403, // Forbidden
errorMessage: 'sampleConnector.' + functionName + '(): missing header - X-CEC-BearerToken'
});
}
if (checks) {
// handle required URL parameter checks
(checks.requiredParameters || []).forEach(function (requiredParameter) {
if (!args.params || !args.params[requiredParameter]) {
reject({
errorCode: 400, // Bad Request
errorMessage: 'sampleConnector.' + functionName + '(): missing required parameter - ' + requiredParameter
});
}
});
// handle required headers parameter checks
(checks.requiredHeaders || []).forEach(function (headerParameter) {
if (!args.headers || !args.headers[headerParameter]) {
reject({
errorCode: 400, // Bad Request
errorMessage: 'sampleConnector.' + functionName + '(): missing required header - ' + headerParameter
});
}
});
// handle required body (data) parameter checks
(checks.requiredData || []).forEach(function (dataParameter) {
if (!args.data || !args.data[dataParameter]) {
reject({
errorCode: 400, // Bad Request
errorMessage: 'sampleConnector.' + functionName + '(): missing required form data - ' + dataParameter
});
}
});
// now get the community if required
if (requiresCommunity) {
translationProvider.getCommunity(args.BearerToken).then(function (communityId) {
args.communityId = communityId;
resolve(args);
}).catch(function (error) {
console.log('sampleConnector.' + functionName + '(): failed to get communityId');
console.log('error', error);
reject(error);
});
} else {
resolve(args);
}
}
});
};
// Internal function get Additional Data from the header.
SampleConnector.prototype.getAdditionalData = function(req) {
var additionalDataJson = '',
additionalData = req.get('X-CEC-AdditionalData') || req.get('AdditionalData');
if (additionalData) {
// Connector framework doesn't allow : and , characters. They are replaced by = and ;, respectively.
// Reverse that in here.
// Replace = by :
// Replace ; by ,
//
// E.g. [{"parameter"="purchase_order";"name"="Purchase Order";"value"="1234567890";"readonly"=true};{"parameter"="business_unit";"name"="Business Unit";"value"="Marketing";"readonly"=false};{"parameter"="due_date";"name"="Due Date";"value"=1590811200000;"readonly"=false}]
// Cases of the three replace statements:
//
// 1. Boolean value that doesn't start with "
// readonly"=false
// to readonly":false
//
// 2. Between two properties
// Marketing";"readonly
// to Marketing","readonly
// or value"=1590811200000;"
// to value"=1590811200000,"
//
// 3. Between two objects
// true};{"parameter
// to true},{"parameter
var additionalDataAsJsonString = additionalData.replace(/\"=/g, '":');
additionalDataAsJsonString = additionalDataAsJsonString.replace(/;\"/g, ',"');
additionalDataAsJsonString = additionalDataAsJsonString.replace(/\};\{/g, '},{');
additionalDataJson = JSON.parse(additionalDataAsJsonString);
}
return additionalDataJson;
};
/**
* @typedef {Object} JobStatus
* @memberof SampleConnector
* @property {object} properties Properties of the job
* @property {string} properties.id The connector job identifier for the requested job.
* @property {string} properties.title The connector job label created for this job identifier.
* @property {string} properties.workflowId The identifier for the workflow to use when translating files in this job.
* @property {('CREATED'|'IMPORTING'|'TRANSLATING'|'DOWNLOADING'|'TRANSLATED')} properties.status The status of the job.
* @property {number} properties.progress Percentage progress through the translation of the job.
*/
/**
* @typedef {Object} ZipFileStream
* @memberof SampleConnector
* @property {string} [Content-Type='application/zip'] Content Type Response Header - application type
* @property {string} [Content-Length={size of zip file}] Content Length Response Header - size of the zip file
* @property {stream} zipfile Contents of the zip file
*/
/**
* Get the translation connector job details, status and translation progress that corresponds to the given job ID from a GET request.
* The corresponding REST API endpoint is: GET: /connector/rest/api/v1/job/:id
* @param {object} req - HTTPS request object.
* @param {object} req.params - Parameters passed on the URL.
* @param {string} req.params.id - Identifier for the connector job to retrieve.
* @param {object} res.header - Parameters passed on via the header.
* @param {string} res.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
* @param {object} res - HTTPS response object.
* @returns {HttpResponse.<SampleConnector.JobStatus>}
*/
SampleConnector.prototype.getJob = function (req, res) {
// validate the request parameters
this.validateRequest(req, {
functionName: 'getJob',
requiredParameters: ['id']
}).then(function (args) {
// get the parameters
var jobId = args.params.id;
// get the meta-data for the job
persistenceStore.getJob({
jobId: jobId
}).then(function (jobMetadata) {
var response = responses.formatResponse('GET', '/v1/job/' + jobId, {
jobId: jobMetadata.properties.id,
jobTitle: jobMetadata.name,
workflowId: jobMetadata.workflowId,
jobStatus: jobMetadata.status,
jobProgress: jobMetadata.progress,
jobStatusMessage: jobMetadata.statusMessage
});
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(response));
}).catch(function (error) {
// request to get the job
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
}).catch(function (error) {
// request failed parameter validation, reject it
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
};
/**
* Create a translation job from a POST request.
* The job is stored in the connector persistence store and will be used to correlate translation requests from OCE to the Language Service Provider.
* The job "status" is set to "CREATED".
* The corresponding REST API endpoint is: POST: /connector/rest/api/v1/job
* @param {object} req - HTTPS request object.
* @param {object} req.body - Body object extracted from the 'req' parameter.
* @param {string} req.body.name - Name of the job to create.
* @param {object} req.header - Parameters passed on via the header.
* @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
* @param {string} req.header.workflowId - The identifier for the workflow to use when translating files in this job.
* @param {object} res - HTTPS response object.
* @returns {HttpResponse.<SampleConnector.JobStatus>}
*/
SampleConnector.prototype.createJob = function (req, res) {
var self = this;
// get the workflow ID
var workflowId = req.get('X-CEC-WorkflowId') || req.get('WorkflowId');
if (!workflowId) {
var error = {
errorCode: 400, // Bad Request
errorMessage: 'sampleConnector.createJob(): missing required header - X-CEC-WorkflowId'
};
// failed to create the job
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
return;
}
// validate the rest of the request parameters
self.validateRequest(req, {
functionName: 'createJob',
requiredData: ['name']
}).then(function (args) {
// get the parameters
var jobName = args.data.name,
additionalData = self.getAdditionalData(req);
// create a new job
persistenceStore.createJob({
jobName: jobName,
workflowId: workflowId,
authToken: args.BearerToken,
additionalData: additionalData
}).then(function (newJob) {
// create a project in the LSP server for this job
var projectName = args.data.name + Math.floor(100000 + Math.random() * 900000);
translationProvider.addProject(args.BearerToken, args.communityId, workflowId, projectName).then(function (projectDetails) {
// update the job details with the project details
newJob.properties.projectId = projectDetails.properties.id;
// save the updated job details
persistenceStore.updateJob(newJob).then(function (jobJSON) {
// format and return the response
var response = responses.formatResponse('POST', '/v1/job', {
jobName: jobJSON.name,
jobId: jobJSON.properties.id,
projectId: jobJSON.properties.projectId,
workflowId: jobJSON.workflowId,
jobStatus: jobJSON.status
});
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(response));
}).catch(function (error) {
// failed to update the job
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
});
}).catch(function (error) {
// failed to create the job
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
}).catch(function (error) {
// request failed parameter validation, reject it
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
};
/**
* Translate the POSTed zip from the request body using the connector job identifier passed on the URL.
* The corresponding REST API endpoint is: POST: /connector/rest/api/v1/job/:id/translate
* The job "status" is updated to "IMPORTING" until all files have been extracted from the zip and corresponding documents created in the Language Service Provider.
* @param {object} req - HTTPS request object.
* @param {object} req.params - Parameters passed on the URL.
* @param {string} req.params.id - Identifier for the connector job to retrieve.
* @param {stream} req.body - Contents of the zip file passed as a stream
* @param {object} req.header - Parameters passed on via the header.
* @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
* @param {object} res - HTTPS response object.
* @returns {HttpResponse.<SampleConnector.JobStatus>}
*/
SampleConnector.prototype.translateJob = function (req, res) {
var self = this;
// validate the request parameters
this.validateRequest(req, {
functionName: 'translateJob',
requiredParameters: ['id']
}).then(function (args) {
// get the parameters
var jobId = args.params.id;
// get the metadata for this job
persistenceStore.getJob({
jobId: jobId
}).then(function (origJobMetadata) {
// get the zip content
var body,
data = [];
req.on('data', function(chunk) {
data.push(Buffer.from(chunk, 'binary'));
});
req.on('end', function() {
body = Buffer.concat(data);
// import the zip file
persistenceStore.importSourceZip({
jobId: jobId,
zipFile: body
}).then(function () {
// zipfile saved
// update status to "IMPORTING"
origJobMetadata.status = "IMPORTING";
persistenceStore.updateJob(origJobMetadata).then(function (jobMetadata) {
// kickoff the job translation background task
jobManager.translateJob(jobMetadata);
// return the updated job details
var response = responses.formatResponse('POST', '/v1/job/' + jobId + '/translate', {
jobId: jobMetadata.properties.id,
jobTitle: jobMetadata.name,
workflowId: jobMetadata.workflowId,
projectId: jobMetadata.projectId,
jobStatus: jobMetadata.status
});
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(response));
}).catch(function (error) {
console.log('sampleConnector.translateJob(): failed to update job status');
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
}).catch(function (error) {
console.log('sampleConnector.translateJob(): failed to save zip file');
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
});
}).catch(function (error) {
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
}).catch(function (error) {
// request failed parameter validation, reject it
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
};
/**
* Refreshes (re-fetches) the translation files from the LSP server for the job ID passed on the URL.
* The corresponding REST API endpoint is: POST: /connector/rest/api/v1/job/:id/translate
* The job "status" is updated to "IMPORTING" until all files have been extracted from the zip and corresponding documents created in the Language Service Provider.
* @param {object} req - HTTPS request object.
* @param {object} req.params - Parameters passed on the URL.
* @param {string} req.params.id - Identifier for the connector job to retrieve.
* @param {stream} req.body - Contents of the zip file passed as a stream
* @param {object} req.header - Parameters passed on via the header.
* @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
* @param {object} res - HTTPS response object.
* @returns {HttpResponse.<SampleConnector.JobStatus>}
*/
SampleConnector.prototype.refreshTranslation = function (req, res) {
var self = this;
// validate the request parameters
this.validateRequest(req, {
functionName: 'refreshTranslation',
requiredParameters: ['id']
}).then(function (args) {
// get the parameters
var jobId = args.params.id;
// get the metadata for this job
persistenceStore.getJob({
jobId: jobId
}).then(function (origJobMetadata) {
// status must be "TRANSLATED" before we can switch to "DOWNLOADING"
if (origJobMetadata.status === 'TRANSLATED') {
// update status to "DOWNLOADING" and change the progress
origJobMetadata.status = "DOWNLOADING";
origJobMetadata.progress = '95';
persistenceStore.updateJob(origJobMetadata).then(function (jobMetadata) {
// kickoff the job translation background task
jobManager.translateJob(jobMetadata);
// return the updated job details
var response = responses.formatResponse('POST', '/v1/job/' + jobId + '/refreshTranslation', {
jobId: jobMetadata.properties.id,
jobTitle: jobMetadata.name,
workflowId: jobMetadata.workflowId,
projectId: jobMetadata.projectId,
jobStatus: jobMetadata.status,
jobProgress: jobMetadata.progress
});
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(response));
}).catch(function (error) {
console.log('sampleConnector.refreshTranslation(): failed to update job status');
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
} else {
// can't switch to "DOWNLOADING" at this time
res.writeHead(400, "Cannot refresh translations for job when job status is not 'TRANSLATED'");
res.end();
}
}).catch(function (error) {
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
}).catch(function (error) {
// request failed parameter validation, reject it
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
};
/**
* Get the translated zip file for the specified connector job identifier.
* The corresponding REST API endpoint is: GET: /connector/rest/api/v1/job/:id/translation
* @param {object} req - HTTPS request object.
* @param {object} req.params - Parameters passed on the URL.
* @param {string} req.params.id - Identifier for the connector job to retrieve.
* @param {object} req.header - Parameters passed on via the header.
* @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
* @param {object} res - HTTPS response object.
* @returns {HttpResponse.<SampleConnector.ZipFileStream>}
*/
SampleConnector.prototype.getTranslation = function (req, res) {
// validate the request parameters
this.validateRequest(req, {
functionName: 'getTranslation',
requiredParameters: ['id']
}).then(function (args) {
// get the parameters
var jobId = args.params.id;
// export the translation zip file for the job
persistenceStore.exportTranslationZip({
jobId: jobId
}).then(function (zipInfo) {
// update the response header
res.writeHead(200, {
'Content-Disposition': 'attachment;filename=translatedJob.zip',
'Content-Type': 'application/zip',
'Content-Length': zipInfo.size
});
// pipe the zip stream to the response
zipInfo.stream.pipe(res);
}).catch(function (error) {
// request failed parameter validation, reject it
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
}).catch(function (error) {
// request failed parameter validation, reject it
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
};
/**
* Remove the job and all associated data from the translation connector
* The corresponding REST API endpoint is: DELETE: /connector/rest/api/v1/job/:id
* @param {object} req - HTTPS request object.
* @param {object} req.params - Parameters passed on the URL.
* @param {string} req.params.id - Identifier for the connector job to retrieve.
* @param {object} req.header - Parameters passed on via the header.
* @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
* @param {object} res - HTTPS response object.
* @returns {HttpResponse.<string>} confirmation that deletion succeeded
*/
SampleConnector.prototype.deleteJob = function (req, res) {
// validate the request parameters
this.validateRequest(req, {
functionName: 'deleteJob',
requiredParameters: ['id']
}).then(function (args) {
// get the parameters
var jobId = args.params.id;
persistenceStore.deleteJob({
jobId: jobId
}).then(function () {
res.writeHead(200);
res.end(JSON.stringify({
message: 'sampleConnector.deleteJob(): Successfully removed job: ' + jobId
}));
}).catch(function (error) {
// request failed parameter validation, reject it
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
}).catch(function (error) {
// request failed parameter validation, reject it
res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
res.end();
});
};
/**
* Validate that the Basic Auth security is enabled and working
* The corresponding REST API endpoint is: GET: /connector/rest/api/v1/authorization/basicAuthorization
* @param {object} req - HTTPS request object.
* @param {object} req.header - Parameters passed on via the header.
* @param {string} req.header.X-CEC-UserName - Username for basic auth validation.
* @param {string} req.header.X-CEC-Pwd - Password to use for basic auth validation.
* @param {object} res - HTTPS response object.
* @returns {HttpResponse.<true>} confirmation that can connect using basic auth
*/
SampleConnector.prototype.validateBasicAuthorization = function (req, res) {
// endpoints are secured in sampleBasicAuth.js, if we got to here, it passed validation
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(true));
};
/**
* Return the array of API versions supported by this server
* The corresponding REST API endpoint is: GET: /connector/rest/api
* @param {object} req - HTTPS request object.
* @param {object} res - HTTPS response object.
* @returns {HttpResponse.<array>} ["v1"]
*/
SampleConnector.prototype.getApiVersions = function (req, res) {
// only support 'v1'
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(['v1']));
};
/**
* Return the connector configuration of this translation connector server
* The corresponding REST API endpoint is: GET: /connector/rest/api/v1/server
* @param {object} req - HTTPS request object.
* @param {object} res - HTTPS response object.
* @returns {HttpResponse.<object>} JSON describing the server configuration
*/
SampleConnector.prototype.getServer = function (req, res) {
// format and return the response
var response = responses.formatResponse('GET', '/v1/server', {
"localizedConnectorName": config.localizedConnectorName,
"connectorAbout": config.connectorAbout,
"locale": config.locale,
"localizedConnectorAbout": config.localizedConnectorAbout,
"authenticationType": config.authenticationType,
"connectorServiceProviderName": config.connectorServiceProviderName,
"connectorVersion": config.connectorVersion
});
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(response));
};
// Export the sample connector
module.exports = new SampleConnector();