ThumbnailService.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2018, 2019
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. 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, _, $) {
  8. /**
  9. * Unique DOM attributes to strip.
  10. * Every time a new thumbnail is generated, it contains unique bits of
  11. * information. For performance reasons, we need to compare the existing
  12. * thumbnail data with the one about to be stored. The unique attributes
  13. * need to be stripped for the compared data, or the comparison will always
  14. * yield false positives.
  15. * @type {Array}
  16. */
  17. var UNIQUE_DOM_ATTRIBUTES = ['id', 'clip-path'];
  18. var ImageCapture = ImageCaptureLib.default;
  19. var EchoCacheService = EchoCacheLib.EchoCacheService;
  20. var ThumbnailService = Class.extend({
  21. /**
  22. * local store
  23. * serves as a cache to store all thumbnails
  24. * @type {ThumbnailStore}
  25. */
  26. _store: null,
  27. /**
  28. * Echo cache service
  29. *
  30. * @type {EchoCacheService}
  31. */
  32. echoCacheService: null,
  33. /**
  34. * Thumbnail service class that has all the relevant logic regarding
  35. * the persistence of thumbnails
  36. *
  37. * @param {Object} [options] - options
  38. * @param {Object} [options.store] - store to initialize the instance with
  39. */
  40. init: function init() {
  41. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  42. ThumbnailService.inherited('init', this, arguments);
  43. this._pendingRequests = {
  44. fetch: {}
  45. };
  46. this.readyPromise = this.initialize(options);
  47. },
  48. /**
  49. * Check whether the pending request type is valid or not
  50. *
  51. * @param {string} type - pending request type to validate
  52. *
  53. * @return {boolean} TRUE if pending request type is valid and FALSE if not
  54. */
  55. _isValidPendingRequestType: function _isValidPendingRequestType(type) {
  56. return !!this._pendingRequests[type];
  57. },
  58. /**
  59. * Set pending request
  60. *
  61. * @param {Object} options - pending request options
  62. * @param {string} options.type - type of pending request; valid values include: 'fetch'
  63. * @param {string} options.cacheId - unique cache id to store the request; if the request already exists,
  64. * @param {Promise} request - Promise that will resolve
  65. */
  66. setPendingRequest: function setPendingRequest(options, request) {
  67. if (!options) {
  68. throw new Error('Invalid options argument provided');
  69. }
  70. var type = options.type,
  71. cacheId = options.cacheId;
  72. if (!cacheId) {
  73. throw new Error('Invalid cacheId option provided');
  74. }
  75. if (!this._isValidPendingRequestType(type)) {
  76. throw new Error('Invalid type: ' + type);
  77. }
  78. this._pendingRequests[type][cacheId] = request;
  79. },
  80. /**
  81. * Get pending request if exists
  82. *
  83. * @param {Object} options - pending request options
  84. * @param {string} options.type - type of pending request; valid values include: 'fetch'
  85. * @param {string} options.cacheId - cache id the previous request was stored under
  86. *
  87. * @return {Promise} Promise that resolves with the result of the same request
  88. */
  89. getPendingRequest: function getPendingRequest(options) {
  90. if (!options) {
  91. throw new Error('Invalid options argument provided');
  92. }
  93. var type = options.type,
  94. cacheId = options.cacheId;
  95. if (!cacheId) {
  96. throw new Error('Invalid cacheId option provided');
  97. }
  98. if (!this._isValidPendingRequestType(type)) {
  99. throw new Error('Invalid type: ' + type);
  100. }
  101. return this._pendingRequests[type][cacheId];
  102. },
  103. /**
  104. * Initialize
  105. *
  106. * @param {object} options - options
  107. * @param {GlassContext} options.glassContext - glass context
  108. * @param {Object} [options.store] - store to use for the thumbnail store
  109. * @param {Object} [options.elementMap] - element map
  110. * @param {EchoCacheService} [options.echoCacheSvc] - echo cache service to use
  111. *
  112. * @return {Promise} promise resolved on success and rejected on failure
  113. */
  114. initialize: function initialize(_ref) {
  115. var glassContext = _ref.glassContext,
  116. _ref$store = _ref.store,
  117. store = _ref$store === undefined ? {} : _ref$store,
  118. elementMap = _ref.elementMap,
  119. echoCacheSvc = _ref.echoCacheSvc;
  120. this.glassContext = glassContext;
  121. this._store = new ThumbnailStore(store);
  122. this.echoCacheService = echoCacheSvc || new EchoCacheService({
  123. logger: glassContext.getCoreSvc('.Logger'),
  124. ajaxSvc: glassContext.getCoreSvc('.Ajax')
  125. });
  126. this._thumbnailGenerator = null;
  127. this._elements = {
  128. map: {},
  129. paths: elementMap
  130. };
  131. return this.initializeThumbnailGenerator();
  132. },
  133. /**
  134. * Initialize thumbnail generator
  135. *
  136. * @return {Promise} promise resolved on success and rejected on failure
  137. */
  138. initializeThumbnailGenerator: function initializeThumbnailGenerator() {
  139. var _this = this;
  140. var promise = void 0;
  141. if (this._elements.paths) {
  142. var elementKeys = Object.keys(this._elements.paths);
  143. promise = Promise.all(elementKeys.map(function (key) {
  144. return 'text!' + _this._elements.paths[key];
  145. }).map(function (path) {
  146. return ClassFactory.loadModule(path);
  147. })).then(function (elements) {
  148. elements.forEach(function (element, key) {
  149. var mapKey = elementKeys[key];
  150. if (mapKey) {
  151. _this._elements.map[mapKey] = element;
  152. }
  153. });
  154. }).catch(function (error) {
  155. _this.glassContext.getCoreSvc('.Logger').error('ThumbnailService.readyPromise:error', error);
  156. });
  157. } else {
  158. promise = Promise.resolve();
  159. }
  160. return promise.then(function () {
  161. _this._thumbnailGenerator = new ImageCapture({
  162. UniqueId: UniqueId,
  163. elementMap: _this._elements.map
  164. });
  165. });
  166. },
  167. /**
  168. * Destructor
  169. */
  170. destroy: function destroy() {
  171. if (this._thumbnailGenerator) {
  172. this._thumbnailGenerator.destroy();
  173. }
  174. this._elements = null;
  175. this._store = null;
  176. },
  177. /**
  178. * Add a thumbnail
  179. *
  180. * @param {string} widgetModel - widget model
  181. * @param {any} data - thumbnail data
  182. *
  183. * @return {Promise} Promise to be resolved on success and rejected with error
  184. */
  185. sync: function sync(widgetModel, data) {
  186. var _this2 = this;
  187. var result = void 0;
  188. if (data) {
  189. var updateToEchoService = function updateToEchoService(cacheId, data) {
  190. return _this2.update(cacheId, data).then(function (_ref2) {
  191. var cacheId = _ref2.cacheId,
  192. affinity = _ref2.affinity;
  193. return _this2.fetch(cacheId, affinity);
  194. }).then(function (_ref3) {
  195. var data = _ref3.data,
  196. cacheId = _ref3.cacheId;
  197. _this2._setCacheIdInWidgetAndStore(widgetModel, cacheId, data);
  198. }, function () {
  199. _this2._setCacheIdInWidgetAndStore(widgetModel, null, null); //remove previous thumbnail on failure
  200. });
  201. };
  202. result = this.fetch(widgetModel.thumbnailId, undefined, { silent: true }).then(function (_ref4) {
  203. var oldData = _ref4.data;
  204. if (_this2._needsUpdate(oldData, data)) {
  205. return updateToEchoService(widgetModel.thumbnailId, data);
  206. } else {
  207. var cachedData = _this2._store.getLocal(widgetModel.id);
  208. if (_this2._needsUpdate(cachedData, data)) {
  209. _this2._setCacheIdInWidgetAndStore(widgetModel, widgetModel.thumbnailId, data);
  210. }
  211. return { data: data };
  212. }
  213. }).catch(function () {
  214. return updateToEchoService(widgetModel.thumbnailId, data);
  215. });
  216. } else {
  217. this._setCacheIdInWidgetAndStore(widgetModel, null /*cacheId*/);
  218. }
  219. return result || Promise.resolve();
  220. },
  221. _setCacheIdInWidgetAndStore: function _setCacheIdInWidgetAndStore(widgetModel, cacheId, data) {
  222. var widgetId = widgetModel.id;
  223. var options = {
  224. payloadData: {
  225. runtimeOnly: true
  226. }
  227. };
  228. widgetModel.set({
  229. thumbnailId: cacheId
  230. }, options);
  231. if (cacheId) {
  232. this._store.set(widgetId, { data: data, cacheId: cacheId });
  233. } else {
  234. this._store.delete(widgetId);
  235. }
  236. },
  237. /**
  238. * Set error on the widget
  239. *
  240. * @param {string} widgetId - widget id
  241. * @param {Error} error - thumbnail error to store
  242. */
  243. setError: function setError(widgetId, error) {
  244. this._store.setError(widgetId, error);
  245. },
  246. /**
  247. * Set warning on the widget
  248. *
  249. * @param {string} widgetId - widget id
  250. * @param {Warning} warning - thumbnail warning to store
  251. */
  252. setWarning: function setWarning(widgetId, warning) {
  253. this._store.setWarning(widgetId, warning);
  254. },
  255. /**
  256. * Check whether the thumbnail is in dire need of an update
  257. *
  258. * @param {Object} data - old thumnail data
  259. * @param {Object} newData - new thumbnail data
  260. *
  261. * @return {boolean} TRUE if needs update and FALSE if not
  262. */
  263. _needsUpdate: function _needsUpdate(data, newData) {
  264. return this._stripUniqueAttributes(data) !== this._stripUniqueAttributes(newData) || this._getImgSrc(data) !== this._getImgSrc(newData);
  265. },
  266. _getImgSrc: function _getImgSrc(data) {
  267. var $data = $(data).find('img');
  268. if ($data) {
  269. return $data.attr('src');
  270. }
  271. return null;
  272. },
  273. /**
  274. * Strip unique attributes from the data
  275. *
  276. * @param {string} data - input data
  277. *
  278. * @return {string} output data with the unique DOM attributes stripped
  279. */
  280. _stripUniqueAttributes: function _stripUniqueAttributes(data) {
  281. var $data = $(data);
  282. UNIQUE_DOM_ATTRIBUTES.forEach(function (attr) {
  283. $data.find('[' + attr + ']').attr(attr, '');
  284. });
  285. return $data.html();
  286. },
  287. /**
  288. * Get store API
  289. *
  290. * @param {Object} options
  291. * @param {Function} options.defaultFn - default function to use in case there's no thumbnail with a specific id
  292. *
  293. * @return {object} store API
  294. */
  295. getStoreAPI: function getStoreAPI() {
  296. var _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { defaultFn: function defaultFn() {} },
  297. defaultFn = _ref5.defaultFn;
  298. // clone the store instance and assign the `getVisTypeDefault` method
  299. return _.extend({}, this._store, {
  300. getVisTypeDefault: defaultFn
  301. });
  302. },
  303. /**
  304. * Get thumbnail base on the thumbnail model and vis type
  305. *
  306. * @param {WidgetModel} widgetModel - widget model
  307. * @param {string} type - vis type
  308. *
  309. * @return {Promise}
  310. */
  311. get: function get(widgetModel, type, storeAPI) {
  312. var _this3 = this;
  313. if (!storeAPI) {
  314. storeAPI = this._store;
  315. }
  316. var id = widgetModel.id;
  317. var promise = void 0;
  318. if (storeAPI.exists(id)) {
  319. promise = Promise.resolve(storeAPI.getLocal(id));
  320. } else {
  321. var cacheId = widgetModel.thumbnailId;
  322. if (cacheId) {
  323. promise = this.fetch(cacheId, undefined, { silent: true }).then(function (_ref6) {
  324. var data = _ref6.data,
  325. cacheId = _ref6.cacheId;
  326. _this3._store.set(id, {
  327. data: data,
  328. cacheId: cacheId
  329. });
  330. return storeAPI.getLocal(id);
  331. }).catch(function () {
  332. // retrieval from cache failed
  333. // but don't reject, recover
  334. return undefined;
  335. });
  336. } else {
  337. promise = Promise.resolve();
  338. }
  339. }
  340. return promise.then(function () {
  341. return storeAPI.get(id, type);
  342. });
  343. },
  344. // --- AJAX methods
  345. /**
  346. * Fetch a specific thumbnail
  347. *
  348. * @param {string} cacheId - the thumbnail id
  349. * @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.
  350. * @param {object} [options={}] - options
  351. * @param {boolean} [options.silent] - silent (don't log failures)
  352. *
  353. * @return {Promise} Promise that resolves with the thumbnail or rejects with error
  354. */
  355. fetch: function fetch(cacheId, affinity) {
  356. var _this4 = this;
  357. var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  358. if (!cacheId) {
  359. return Promise.reject(new Error('Invalid cacheId argument provided: ' + cacheId));
  360. }
  361. var pendingRequest = this.getPendingRequest({
  362. type: 'fetch',
  363. cacheId: cacheId
  364. });
  365. if (pendingRequest) {
  366. var isPromise = !!pendingRequest.then;
  367. if (isPromise) {
  368. // if promise, return as is
  369. return pendingRequest;
  370. } else {
  371. // it's an error
  372. return Promise.reject(pendingRequest);
  373. }
  374. }
  375. var request = this.echoCacheService.retrieve(cacheId, affinity).then(function (response) {
  376. // clear the pending request
  377. _this4.setPendingRequest({
  378. type: 'fetch',
  379. cacheId: cacheId
  380. }, undefined);
  381. var text = response.text;
  382. _this4.glassContext.getCoreSvc('.Logger').debug('ThumbnailService.fetch:response', response);
  383. return {
  384. cacheId: cacheId,
  385. data: text
  386. };
  387. }).catch(function (response) {
  388. var error = _this4._ajaxErrorHandler(response, 'ThumbnailService.fetch:error', options);
  389. // save the error as a pending request, to prevent subsequent request before any update has finished
  390. // @see ThumbnailService::update method
  391. _this4.setPendingRequest({
  392. type: 'fetch',
  393. cacheId: cacheId
  394. }, error);
  395. // reject the promise with the error.
  396. return Promise.reject(error);
  397. });
  398. this.setPendingRequest({
  399. type: 'fetch',
  400. cacheId: cacheId
  401. }, request);
  402. return request;
  403. },
  404. /**
  405. * Update a specific thumbnail
  406. *
  407. * @param {string} cacheId - the id of thumbnail
  408. * @param {string} thumbnail - thumbnail data
  409. * @param {object} [options={}] - options
  410. * @param {boolean} [options.silent] - silent (don't log failures)
  411. *
  412. * @return {Promise} Promise that resolves with the thumbnail or rejects with error
  413. */
  414. update: function update(cacheId, thumbnail, options) {
  415. var _this5 = this;
  416. return this.echoCacheService.stash(thumbnail, {
  417. cacheId: cacheId
  418. }).then(function (_ref7) {
  419. var id = _ref7.id,
  420. location = _ref7.location,
  421. affinity = _ref7.affinity;
  422. // clear the pending request on failure
  423. _this5.setPendingRequest({
  424. type: 'fetch',
  425. cacheId: id
  426. }, undefined);
  427. return {
  428. cacheId: id,
  429. location: location,
  430. affinity: affinity
  431. };
  432. }).catch(function (response) {
  433. return Promise.reject(_this5._ajaxErrorHandler(response, 'ThumbnailService.update:error', options));
  434. });
  435. },
  436. /**
  437. * Ajax error handler
  438. *
  439. * @param {Object} response - error response about to be handled
  440. * @param {string} logString - string used for logging
  441. */
  442. _ajaxErrorHandler: function _ajaxErrorHandler(response) {
  443. var logString = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'ThumbnailService:error';
  444. var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : { silent: false };
  445. var silent = options.silent;
  446. var message = this._parseError(response, StringResources.get('thumbnails_error_fetching'));
  447. var error = new Error(message);
  448. if (!silent) {
  449. this.glassContext.getCoreSvc('.Logger').error(logString, error);
  450. }
  451. return error;
  452. },
  453. /**
  454. * Parse error response
  455. *
  456. * @param {Object} response - ajax response
  457. * @param {string} response.responseText - ajax response text
  458. * @param {string} defaultMessage - default message in case there is no response text
  459. *
  460. * @return {string} final error text
  461. */
  462. _parseError: function _parseError(response, defaultMessage) {
  463. var message = defaultMessage;
  464. try {
  465. if (response.responseText) {
  466. var text = JSON.parse(response.responseText);
  467. message = text.error;
  468. }
  469. } catch (e) {
  470. // ignore JSON parsing errors
  471. }
  472. return message;
  473. },
  474. /**
  475. * Prepare thumbnail data
  476. *
  477. * @param {string} data - data to be prepared
  478. *
  479. * @return {string} prepared data
  480. */
  481. _prepareThumbnail: function _prepareThumbnail(data) {
  482. return JSON.stringify({ data: data });
  483. },
  484. /**
  485. * Generate image
  486. *
  487. * @param {DOMElement} _domNode - DOM node to generate image of
  488. * @param {object} options - options
  489. * @param {number} maxHeight - maximum height
  490. *
  491. * @return {Promise} Promise to be resolved on success and rejected on faiure
  492. */
  493. generateImage: function generateImage(_domNode, options, maxHeight) {
  494. options = _.extend(options || {}, {
  495. // the thumbnail generator takes the background color of selected node and applies them to
  496. // the thumbnail - we don't want this for nodes with static content, e.g. webpage widgets
  497. onClone: function onClone(doc, node) {
  498. var $staticNode = $(node).find('.staticContent');
  499. if ($staticNode.length) {
  500. $staticNode.css({ 'background-color': 'transparent' });
  501. }
  502. },
  503. excludeEmptyIframes: true,
  504. transparentBackground: false
  505. });
  506. return this._thumbnailGenerator.generateImage(_domNode, options, maxHeight);
  507. },
  508. /**
  509. * Generate image
  510. *
  511. * @param {DOMElement} _domNode - DOM node to generate image of
  512. * @param {object} options - options
  513. * @param {number} maxHeight - maximum height
  514. *
  515. * @return {Promise} Promise to be resolved on success and rejected on faiure
  516. */
  517. generateMarkup: function generateMarkup(_domNode, options, maxHeight) {
  518. return this.generateImage(_domNode, options, maxHeight).then(function (content) {
  519. return '<div class="visThumbnail"><div class="visThumbnailImgContainer"><img src="' + content + '" /></div></div>';
  520. });
  521. },
  522. /**
  523. * Delete thumbnail
  524. *
  525. * @param {id} thumbnail id
  526. * @return {ThumbnailStore} self; current instance
  527. */
  528. delete: function _delete(id) {
  529. return this._store.delete(id);
  530. }
  531. });
  532. return ThumbnailService;
  533. });
  534. //# sourceMappingURL=ThumbnailService.js.map