'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2018, 2019 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['../../../lib/@waca/core-client/js/core-client/ui/core/Class', '../../../app/nls/StringResources', './ThumbnailStore', '../../../lib/@waca/core-client/js/core-client/utils/UniqueId', '../../../lib/@waca/image-capture/dist/js/bundles/image-capture.min', '../../../lib/@waca/echo-cache/dist/js/bundles/echo-cache.min', '../../../lib/@waca/core-client/js/core-client/utils/ClassFactory', 'underscore', 'jquery'], function (Class, StringResources, ThumbnailStore, UniqueId, ImageCaptureLib, EchoCacheLib, ClassFactory, _, $) { /** * Unique DOM attributes to strip. * Every time a new thumbnail is generated, it contains unique bits of * information. For performance reasons, we need to compare the existing * thumbnail data with the one about to be stored. The unique attributes * need to be stripped for the compared data, or the comparison will always * yield false positives. * @type {Array} */ var UNIQUE_DOM_ATTRIBUTES = ['id', 'clip-path']; var ImageCapture = ImageCaptureLib.default; var EchoCacheService = EchoCacheLib.EchoCacheService; var ThumbnailService = Class.extend({ /** * local store * serves as a cache to store all thumbnails * @type {ThumbnailStore} */ _store: null, /** * Echo cache service * * @type {EchoCacheService} */ echoCacheService: null, /** * Thumbnail service class that has all the relevant logic regarding * the persistence of thumbnails * * @param {Object} [options] - options * @param {Object} [options.store] - store to initialize the instance with */ init: function init() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; ThumbnailService.inherited('init', this, arguments); this._pendingRequests = { fetch: {} }; this.readyPromise = this.initialize(options); }, /** * Check whether the pending request type is valid or not * * @param {string} type - pending request type to validate * * @return {boolean} TRUE if pending request type is valid and FALSE if not */ _isValidPendingRequestType: function _isValidPendingRequestType(type) { return !!this._pendingRequests[type]; }, /** * Set pending request * * @param {Object} options - pending request options * @param {string} options.type - type of pending request; valid values include: 'fetch' * @param {string} options.cacheId - unique cache id to store the request; if the request already exists, * @param {Promise} request - Promise that will resolve */ setPendingRequest: function setPendingRequest(options, request) { if (!options) { throw new Error('Invalid options argument provided'); } var type = options.type, cacheId = options.cacheId; if (!cacheId) { throw new Error('Invalid cacheId option provided'); } if (!this._isValidPendingRequestType(type)) { throw new Error('Invalid type: ' + type); } this._pendingRequests[type][cacheId] = request; }, /** * Get pending request if exists * * @param {Object} options - pending request options * @param {string} options.type - type of pending request; valid values include: 'fetch' * @param {string} options.cacheId - cache id the previous request was stored under * * @return {Promise} Promise that resolves with the result of the same request */ getPendingRequest: function getPendingRequest(options) { if (!options) { throw new Error('Invalid options argument provided'); } var type = options.type, cacheId = options.cacheId; if (!cacheId) { throw new Error('Invalid cacheId option provided'); } if (!this._isValidPendingRequestType(type)) { throw new Error('Invalid type: ' + type); } return this._pendingRequests[type][cacheId]; }, /** * Initialize * * @param {object} options - options * @param {GlassContext} options.glassContext - glass context * @param {Object} [options.store] - store to use for the thumbnail store * @param {Object} [options.elementMap] - element map * @param {EchoCacheService} [options.echoCacheSvc] - echo cache service to use * * @return {Promise} promise resolved on success and rejected on failure */ initialize: function initialize(_ref) { var glassContext = _ref.glassContext, _ref$store = _ref.store, store = _ref$store === undefined ? {} : _ref$store, elementMap = _ref.elementMap, echoCacheSvc = _ref.echoCacheSvc; this.glassContext = glassContext; this._store = new ThumbnailStore(store); this.echoCacheService = echoCacheSvc || new EchoCacheService({ logger: glassContext.getCoreSvc('.Logger'), ajaxSvc: glassContext.getCoreSvc('.Ajax') }); this._thumbnailGenerator = null; this._elements = { map: {}, paths: elementMap }; return this.initializeThumbnailGenerator(); }, /** * Initialize thumbnail generator * * @return {Promise} promise resolved on success and rejected on failure */ initializeThumbnailGenerator: function initializeThumbnailGenerator() { var _this = this; var promise = void 0; if (this._elements.paths) { var elementKeys = Object.keys(this._elements.paths); promise = Promise.all(elementKeys.map(function (key) { return 'text!' + _this._elements.paths[key]; }).map(function (path) { return ClassFactory.loadModule(path); })).then(function (elements) { elements.forEach(function (element, key) { var mapKey = elementKeys[key]; if (mapKey) { _this._elements.map[mapKey] = element; } }); }).catch(function (error) { _this.glassContext.getCoreSvc('.Logger').error('ThumbnailService.readyPromise:error', error); }); } else { promise = Promise.resolve(); } return promise.then(function () { _this._thumbnailGenerator = new ImageCapture({ UniqueId: UniqueId, elementMap: _this._elements.map }); }); }, /** * Destructor */ destroy: function destroy() { if (this._thumbnailGenerator) { this._thumbnailGenerator.destroy(); } this._elements = null; this._store = null; }, /** * Add a thumbnail * * @param {string} widgetModel - widget model * @param {any} data - thumbnail data * * @return {Promise} Promise to be resolved on success and rejected with error */ sync: function sync(widgetModel, data) { var _this2 = this; var result = void 0; if (data) { var updateToEchoService = function updateToEchoService(cacheId, data) { return _this2.update(cacheId, data).then(function (_ref2) { var cacheId = _ref2.cacheId, affinity = _ref2.affinity; return _this2.fetch(cacheId, affinity); }).then(function (_ref3) { var data = _ref3.data, cacheId = _ref3.cacheId; _this2._setCacheIdInWidgetAndStore(widgetModel, cacheId, data); }, function () { _this2._setCacheIdInWidgetAndStore(widgetModel, null, null); //remove previous thumbnail on failure }); }; result = this.fetch(widgetModel.thumbnailId, undefined, { silent: true }).then(function (_ref4) { var oldData = _ref4.data; if (_this2._needsUpdate(oldData, data)) { return updateToEchoService(widgetModel.thumbnailId, data); } else { var cachedData = _this2._store.getLocal(widgetModel.id); if (_this2._needsUpdate(cachedData, data)) { _this2._setCacheIdInWidgetAndStore(widgetModel, widgetModel.thumbnailId, data); } return { data: data }; } }).catch(function () { return updateToEchoService(widgetModel.thumbnailId, data); }); } else { this._setCacheIdInWidgetAndStore(widgetModel, null /*cacheId*/); } return result || Promise.resolve(); }, _setCacheIdInWidgetAndStore: function _setCacheIdInWidgetAndStore(widgetModel, cacheId, data) { var widgetId = widgetModel.id; var options = { payloadData: { runtimeOnly: true } }; widgetModel.set({ thumbnailId: cacheId }, options); if (cacheId) { this._store.set(widgetId, { data: data, cacheId: cacheId }); } else { this._store.delete(widgetId); } }, /** * Set error on the widget * * @param {string} widgetId - widget id * @param {Error} error - thumbnail error to store */ setError: function setError(widgetId, error) { this._store.setError(widgetId, error); }, /** * Set warning on the widget * * @param {string} widgetId - widget id * @param {Warning} warning - thumbnail warning to store */ setWarning: function setWarning(widgetId, warning) { this._store.setWarning(widgetId, warning); }, /** * Check whether the thumbnail is in dire need of an update * * @param {Object} data - old thumnail data * @param {Object} newData - new thumbnail data * * @return {boolean} TRUE if needs update and FALSE if not */ _needsUpdate: function _needsUpdate(data, newData) { return this._stripUniqueAttributes(data) !== this._stripUniqueAttributes(newData) || this._getImgSrc(data) !== this._getImgSrc(newData); }, _getImgSrc: function _getImgSrc(data) { var $data = $(data).find('img'); if ($data) { return $data.attr('src'); } return null; }, /** * Strip unique attributes from the data * * @param {string} data - input data * * @return {string} output data with the unique DOM attributes stripped */ _stripUniqueAttributes: function _stripUniqueAttributes(data) { var $data = $(data); UNIQUE_DOM_ATTRIBUTES.forEach(function (attr) { $data.find('[' + attr + ']').attr(attr, ''); }); return $data.html(); }, /** * Get store API * * @param {Object} options * @param {Function} options.defaultFn - default function to use in case there's no thumbnail with a specific id * * @return {object} store API */ getStoreAPI: function getStoreAPI() { var _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { defaultFn: function defaultFn() {} }, defaultFn = _ref5.defaultFn; // clone the store instance and assign the `getVisTypeDefault` method return _.extend({}, this._store, { getVisTypeDefault: defaultFn }); }, /** * Get thumbnail base on the thumbnail model and vis type * * @param {WidgetModel} widgetModel - widget model * @param {string} type - vis type * * @return {Promise} */ get: function get(widgetModel, type, storeAPI) { var _this3 = this; if (!storeAPI) { storeAPI = this._store; } var id = widgetModel.id; var promise = void 0; if (storeAPI.exists(id)) { promise = Promise.resolve(storeAPI.getLocal(id)); } else { var cacheId = widgetModel.thumbnailId; if (cacheId) { promise = this.fetch(cacheId, undefined, { silent: true }).then(function (_ref6) { var data = _ref6.data, cacheId = _ref6.cacheId; _this3._store.set(id, { data: data, cacheId: cacheId }); return storeAPI.getLocal(id); }).catch(function () { // retrieval from cache failed // but don't reject, recover return undefined; }); } else { promise = Promise.resolve(); } } return promise.then(function () { return storeAPI.get(id, type); }); }, // --- AJAX methods /** * Fetch a specific thumbnail * * @param {string} cacheId - the thumbnail id * @param {string} [affinity] - server affinity to use (returned from the update call); cacheId and affinity are closely related. Making assumptions about the affinity is not recommended at this point. * @param {object} [options={}] - options * @param {boolean} [options.silent] - silent (don't log failures) * * @return {Promise} Promise that resolves with the thumbnail or rejects with error */ fetch: function fetch(cacheId, affinity) { var _this4 = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; if (!cacheId) { return Promise.reject(new Error('Invalid cacheId argument provided: ' + cacheId)); } var pendingRequest = this.getPendingRequest({ type: 'fetch', cacheId: cacheId }); if (pendingRequest) { var isPromise = !!pendingRequest.then; if (isPromise) { // if promise, return as is return pendingRequest; } else { // it's an error return Promise.reject(pendingRequest); } } var request = this.echoCacheService.retrieve(cacheId, affinity).then(function (response) { // clear the pending request _this4.setPendingRequest({ type: 'fetch', cacheId: cacheId }, undefined); var text = response.text; _this4.glassContext.getCoreSvc('.Logger').debug('ThumbnailService.fetch:response', response); return { cacheId: cacheId, data: text }; }).catch(function (response) { var error = _this4._ajaxErrorHandler(response, 'ThumbnailService.fetch:error', options); // save the error as a pending request, to prevent subsequent request before any update has finished // @see ThumbnailService::update method _this4.setPendingRequest({ type: 'fetch', cacheId: cacheId }, error); // reject the promise with the error. return Promise.reject(error); }); this.setPendingRequest({ type: 'fetch', cacheId: cacheId }, request); return request; }, /** * Update a specific thumbnail * * @param {string} cacheId - the id of thumbnail * @param {string} thumbnail - thumbnail data * @param {object} [options={}] - options * @param {boolean} [options.silent] - silent (don't log failures) * * @return {Promise} Promise that resolves with the thumbnail or rejects with error */ update: function update(cacheId, thumbnail, options) { var _this5 = this; return this.echoCacheService.stash(thumbnail, { cacheId: cacheId }).then(function (_ref7) { var id = _ref7.id, location = _ref7.location, affinity = _ref7.affinity; // clear the pending request on failure _this5.setPendingRequest({ type: 'fetch', cacheId: id }, undefined); return { cacheId: id, location: location, affinity: affinity }; }).catch(function (response) { return Promise.reject(_this5._ajaxErrorHandler(response, 'ThumbnailService.update:error', options)); }); }, /** * Ajax error handler * * @param {Object} response - error response about to be handled * @param {string} logString - string used for logging */ _ajaxErrorHandler: function _ajaxErrorHandler(response) { var logString = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'ThumbnailService:error'; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : { silent: false }; var silent = options.silent; var message = this._parseError(response, StringResources.get('thumbnails_error_fetching')); var error = new Error(message); if (!silent) { this.glassContext.getCoreSvc('.Logger').error(logString, error); } return error; }, /** * Parse error response * * @param {Object} response - ajax response * @param {string} response.responseText - ajax response text * @param {string} defaultMessage - default message in case there is no response text * * @return {string} final error text */ _parseError: function _parseError(response, defaultMessage) { var message = defaultMessage; try { if (response.responseText) { var text = JSON.parse(response.responseText); message = text.error; } } catch (e) { // ignore JSON parsing errors } return message; }, /** * Prepare thumbnail data * * @param {string} data - data to be prepared * * @return {string} prepared data */ _prepareThumbnail: function _prepareThumbnail(data) { return JSON.stringify({ data: data }); }, /** * Generate image * * @param {DOMElement} _domNode - DOM node to generate image of * @param {object} options - options * @param {number} maxHeight - maximum height * * @return {Promise} Promise to be resolved on success and rejected on faiure */ generateImage: function generateImage(_domNode, options, maxHeight) { options = _.extend(options || {}, { // the thumbnail generator takes the background color of selected node and applies them to // the thumbnail - we don't want this for nodes with static content, e.g. webpage widgets onClone: function onClone(doc, node) { var $staticNode = $(node).find('.staticContent'); if ($staticNode.length) { $staticNode.css({ 'background-color': 'transparent' }); } }, excludeEmptyIframes: true, transparentBackground: false }); return this._thumbnailGenerator.generateImage(_domNode, options, maxHeight); }, /** * Generate image * * @param {DOMElement} _domNode - DOM node to generate image of * @param {object} options - options * @param {number} maxHeight - maximum height * * @return {Promise} Promise to be resolved on success and rejected on faiure */ generateMarkup: function generateMarkup(_domNode, options, maxHeight) { return this.generateImage(_domNode, options, maxHeight).then(function (content) { return '
'; }); }, /** * Delete thumbnail * * @param {id} thumbnail id * @return {ThumbnailStore} self; current instance */ delete: function _delete(id) { return this._store.delete(id); } }); return ThumbnailService; }); //# sourceMappingURL=ThumbnailService.js.map