a49beb2f478f0790942411168bf72411c5052ca6
[archive/20170607/tools/tic.git] / public / src / js / page / package.js
1 define([
2     'jquery',
3     'lodash',
4     'bootstrap-contextmenu',
5     'js/util',
6     'js/page/settings',
7     'js/logger',
8     'js/page/job',
9     'js/model/JobModel'
10 ], function (
11     $,
12     _,
13     bootstrapContextmenu,
14     Util,
15     Settings,
16     Logger,
17     Job,
18     JobModel
19 ) {
20     'use strict';
21
22     var logger = Logger('package.js');
23     var socket;
24
25     var $tree = $('#tic-package-left-col-tree');
26     var defaultFilters = [];
27     var repos = [];
28     var packages = [];
29     var pkgInfo = null;
30     var provides = null;
31     var files = null;
32     var conflicts = null;
33     var groups = null;
34     var groupId = 0;
35
36     var JOB_STATUS_INPROGRESS = 'INPROGRESS';
37
38     // AppConfig
39     var AppConfig = null;
40
41     function onClickHandlerForImgCreationBtn() {
42         logger.info('onClickHandlerForImgCreationBtn');
43
44         var checkedPackagesList, checkedPackgaesCount, newJobModel;
45
46         newJobModel = null;
47         checkedPackagesList = _getChecked();
48         checkedPackgaesCount = _.size(checkedPackagesList);
49
50         // if no selected packages
51         if (checkedPackgaesCount === 0) {
52             return;
53         }
54
55         function doCreateAnImage() {
56             var pathKsFile, imageName;
57             logger.info('onClickHandlerForImgCreationBtn.doCreateAnImage');
58
59             // scroll
60             $('html, body').animate({
61                 scrollTop: $('#tic-job-section').offset().top
62             }, 500);
63
64             // get the name of image
65             pathKsFile = newJobModel.getJobKsPath();
66             imageName = pathKsFile.substring(pathKsFile.lastIndexOf('/')+1, pathKsFile.lastIndexOf('.'));
67
68             // options for the creation
69             var msgData = {
70                 jobId: newJobModel.getJobId(),
71                 pathKsFile: pathKsFile,
72                 pathOutput: newJobModel.getJobPath(),
73                 imageName: imageName + '.tar.gz',
74                 imageArch: newJobModel.getJobArch()
75             };
76
77             // create
78             Job.doCreateAnImage(msgData);
79         }
80
81         function getRecipeFile() {
82             var msgData;
83
84             logger.info('onClickHandlerForImgCreationBtn.getRecipeFile: job_path = ' + newJobModel.getJobPath());
85
86             /**
87              * FIXME
88              *
89              * filename will be removed.
90              * filenmae generated by ticcore
91              */
92             msgData = {
93                 recipes: Settings.getRecipeStore(),
94                 packages: _.map(checkedPackagesList, 'name'),
95                 outdir: newJobModel.getJobPath()
96             };
97
98             function onErrorGetRecipeFile(err) {
99                 logger.info('onClickHandlerForImgCreationBtn.onErrorGetRecipeFile');
100                 logger.error(err);
101                 throw err;
102             }
103
104             function onSuccessGetRecipeFile(responseObj) {
105                 var msgObj, strKsName, strPath, strArch;
106
107                 if (!_.isEmpty(responseObj) && responseObj.result === "false") {
108                     logger.error(responseObj.message);
109                     throw responseObj;
110                 }
111                 strPath = responseObj.data.path;
112                 strKsName = strPath.substring(strPath.lastIndexOf('/') + 1);
113                 strArch = responseObj.data.arch;
114
115                 // update
116                 newJobModel.setJobKs(strKsName);
117                 newJobModel.setJobArch(strArch);
118
119                 logger.info('onClickHandlerForImgCreationBtn.onSuccessGetRecipeFile: ' + responseObj.data.kspath);
120                 msgObj = {
121                     job_status: JOB_STATUS_INPROGRESS,
122                     job_hasksfile: true,
123                     job_ks: strKsName,
124                     job_arch: strArch
125                 };
126                 return Util.POST(AppConfig.EVENT.JOB.JOB_EDIT_ONE + newJobModel.getJobId(), msgObj);
127             }
128
129             return Util.POST(AppConfig.EVENT.PACKAGE.EXPORT, msgData)
130             .then(onSuccessGetRecipeFile)
131             .catch(onErrorGetRecipeFile);
132         }
133
134         function setJobModel(jobItem) {
135             return new Promise(function (resolve, reject) {
136                 var jobObj = jobItem[0];
137                 newJobModel = new JobModel(jobObj);
138                 resolve(newJobModel);
139             });
140         }
141
142         function getJobId(jobItem) {
143             logger.info('onClickHandlerForImgCreationBtn.getJobId');
144             var jobId = jobItem.job_id;
145             return Util.POST(AppConfig.EVENT.JOB.JOB_GET_BYID + jobId);
146         }
147
148         function doConfirmOk() {
149             logger.info('onClickHandlerForImgCreationBtn.addJob');
150             return Util.POST(AppConfig.EVENT.JOB.JOB_ADD_ONE);
151         }
152
153         // confirm dialog
154         function showConfirmDialog() {
155             logger.info('onClickHandlerForImgCreationBtn.showConfirmDialog');
156             return Util.showConfirmDialog('Are you sure want to create an image ?');
157         }
158
159         return showConfirmDialog()
160             .then(doConfirmOk)
161             .then(getJobId)
162             .then(setJobModel)
163             .then(getRecipeFile)
164             .then(doCreateAnImage);
165     }
166
167     function _getChecked() {
168         var checkedList = [];
169         _.forEach(pkgInfo, function(value, key) {
170             if (value.checked === true) {
171                 checkedList.push(value);
172             }
173         });
174         return checkedList;
175     }
176
177     function _find(name, data) {
178         if (_.isEmpty(data)) {
179             data = pkgInfo;
180         }
181         return data[name];
182     }
183
184     function _findFromNode(node) {
185         if ('metaname' in node) {
186             return pkgInfo[node.metaname];
187         } else {
188             return pkgInfo[node.text];
189         }
190     }
191
192     function _setDefaultPackage(defaultPackages) {
193         var nodes = []
194         var pickDefault = _.pick(pkgInfo, defaultPackages);
195         _.forEach(pickDefault, function(value, key) {
196             // One or more of the same nodes can exist
197             _.forEach(value.view, function(node) {
198                 nodes.push(node);
199             });
200             // update local state
201             value.checked = true;
202         });
203
204         $tree.treeview('checkNode', [nodes, { silent: true }]);
205     }
206
207     // Compares two rpm version numbers (e.g. "1.7.1" or "1.2b").
208     // This function is based on https://gist.github.com/em92/d58944f21c68b69433cefb6c49e0defd
209     function versionCompare(v1, v2, options) {
210         if (!v1 && !v2) {
211             return 0;
212         } else if (!v2) {
213             return 1;
214         } else if (!v1) {
215             return -1;
216         }
217
218         var v1parts = v1.split(/[.+-/_]/);
219         var v2parts = v2.split(/[.+-/_]/);
220
221         function compareParts(v1parts, v2parts, options) {
222             var zeroExtend = options && options.zeroExtend;
223
224             if (zeroExtend) {
225                 while (v1parts.length < v2parts.length) v1parts.push("0");
226                 while (v2parts.length < v1parts.length) v2parts.push("0");
227             }
228
229             for (var i = 0; i < v1parts.length; ++i) {
230                 if (v2parts.length === i) {
231                     return 1;
232                 }
233
234                 var v1part = parseInt(v1parts[i]);
235                 var v2part = parseInt(v2parts[i]);
236                 // (NaN === NaN) -> false
237                 var v1part_is_string = !(v1part === v1part);
238                 var v2part_is_string = !(v2part === v2part);
239                 v1part = v1part_is_string ? v1parts[i] : v1part;
240                 v2part = v2part_is_string ? v2parts[i] : v2part;
241
242                 if (v1part_is_string === v2part_is_string) {
243                     if (v1part_is_string === false) {
244                         // integer compare
245                         if (v1part === v2part) {
246                             continue;
247                         } else if (v1part > v2part) {
248                             return 1;
249                         } else {
250                             return -1;
251                         }
252                     } else {
253                         // letters and numbers in string
254                         // split letters and numbers
255                         var v1subparts = v1part.match(/[a-zA-Z]+|[0-9]+/g);
256                         var v2subparts = v2part.match(/[a-zA-Z]+|[0-9]+/g);
257                         if ( (v1subparts.length === 1) && (v2subparts.length === 1) ) {
258                             // only letters in string
259                             v1part = v1subparts[0];
260                             v2part = v2subparts[0];
261                             if (v1part === v2part) {
262                                 continue;
263                             } else if (v1part > v2part) {
264                                 return 1;
265                             } else {
266                                 return -1;
267                             }
268                         }
269                         var result = compareParts(v1subparts, v2subparts);
270                         if (result === 0) {
271                             continue;
272                         } else {
273                             return result;
274                         }
275                     }
276                 } else {
277                     return v2part_is_string ? 1 : -1;
278                 }
279             }
280
281             if (v1parts.length != v2parts.length) {
282                 return -1;
283             }
284             return 0;
285         }
286
287         return compareParts(v1parts, v2parts, options);
288     }
289
290     function _meetRequiredVersion(reqVersion, cmpVersion) {
291         var cmpResult = _comapre_ver(reqVersion, cmpVersion, true);
292         if (cmpResult === 0 && _.includes(['EQ', 'GE', 'LE'], reqVersion.flags)) {
293             return true;
294         } else if (cmpResult === 1 && _.includes(['LT', 'LE'], reqVersion.flags)) {
295             return true;
296         } else if (cmpResult === -1 && _.includes(['GT', 'GE'], reqVersion.flags)) {
297             return true;
298         }
299         return false;
300     }
301
302     function _comapre_ver(ver1, ver2, flag) {
303         // flag === false; Compare which version is up-to-date
304         // flag === true; Check if the required version is met
305
306         var epoch1 = ver1.epoch;
307         var epoch2 = ver2.epoch;
308         // epoch is a number (optional, default=0)
309         epoch1 = epoch1 ? Number(epoch1) : 0;
310         epoch2 = epoch2 ? Number(epoch2) : 0;
311
312         if (epoch1 === epoch2) {
313             var result = versionCompare(ver1.ver, ver2.ver);
314             // version match (ver1 === ver2)
315             if (result === 0) {
316                 if (!flag || ver1.rel) {
317                     return versionCompare(ver1.rel, ver2.rel);
318                 }
319                 return result;
320             }
321             return result;
322         } else if (epoch1 > epoch2) {
323             return 1
324         } else {
325             return -1;
326         }
327     }
328
329     // analyze install-dependency (requires)
330     function _analyzeInstallDependency(checkNode) {
331         var stack = [];
332         var pkg_count = Object.keys(pkgInfo).length
333         var number = 0;
334         var sccNum = 0;
335         var groupNum = 0;
336         var selected = new Array(pkg_count);
337         var scc_id = new Array(pkg_count);
338         var min_num = new Array(pkg_count);
339         var progressStatus = true;
340         var errorMsg = null;
341         var conflictsTable = {};
342
343         // init check type
344         _.forEach(pkgInfo, function(value, key) {
345             // check type: -1(unchecked), 0(checked), 1~N(checking)
346             selected[value.id] = value.checked ? 0 : -1;
347             scc_id[value.id] = 0;
348         });
349
350         function _makeSCC(pkgId) {
351             sccNum += 1;
352             var sccList = [];
353
354             // make scc
355             while(!_.isEmpty(stack)) {
356                 var pkg = stack.pop();
357                 scc_id[pkg.id] = sccNum;
358                 sccList.push(pkg);
359
360                 if (pkg.id === pkgId) {
361                     break;
362                 }
363             }
364
365             // circular depependency
366             if (sccList.length > 1) {
367                 groupId += 1;
368                 var groupPkgList = [];
369                 _.forEach(sccList, function(pkg) {
370                     pkg.group = groupId;
371                     groupPkgList.push(pkg.name);
372                 });
373                 groups[groupId] = groupPkgList;
374                 // logger.info('circular dependency: groupId(' + groupId + '), list(' + groupPkgList + ')');
375             }
376         }
377
378         function _createReference(node1, node2) {
379             if (!_.isEmpty(node1.forward) && _.includes(node1.forward, node2.name))
380                 return
381
382             if (!_.isEmpty(node1.forward)) {
383                 node1.forward.push(node2.name);
384             } else {
385                 node1.forward = [node2.name];
386             }
387
388             if (!_.isEmpty(node2.backward)) {
389                 node2.backward.push(node1.name);
390             } else {
391                 node2.backward = [node1.name];
392             }
393         }
394
395         function _select_files(fileList, require) {
396             if (!fileList || !require){
397                 return null;
398             }
399             var selectpkg = null;
400             // 1. sort in ascending order
401             fileList.sort();
402             // 2-1. Choose the selected rpm
403             for (let fname of fileList) {
404                 let fileInfo = pkgInfo[fname];
405                 if (fileInfo.checked || selected[fileInfo.id] >= 1) {
406                     return fileInfo;
407                 }
408             }
409             // 2-2. Choose the rpm (it does not conflitcs)
410             for (let fname of fileList) {
411                 let fileInfo = pkgInfo[fname];
412                 var checkConflict = _checkConflicts(fileInfo);
413                 if (checkConflict.result === false) {
414                     return fileInfo;
415                 }
416             }
417             return pkgInfo[fileList[0]];
418         }
419
420         function _select_rpm(capability, require, recommend) {
421             var provideList = [];
422             // 1. Choose the rpm included in version from provides
423             if (require.ver) {
424                 _.forEach(capability, function(provide) {
425                     var verData = provide.data;
426                     if(!provide.data.ver) {
427                         var pkg = pkgInfo[provide.name];
428                         verData = pkg.version;
429                     }
430                     if (_meetRequiredVersion(require, verData)) {
431                         // Meet the required version
432                         provideList.push(provide);
433                     }
434                 });
435             } else {
436                 provideList = capability;
437             }
438
439             // error case (the rpm does not exist)
440             if (_.isEmpty(provideList)) {
441                 return null;
442             }
443
444             if (provideList.length === 1) {
445                 return pkgInfo[provideList[0].name];
446             }
447
448             // 2. Select one of the rpms by priority
449             // 2-1. Choose the default rpm or the selected rpm
450             for (var provide of provideList) {
451                 var tmpInfo = pkgInfo[provide.name];
452                 if (tmpInfo.checked || selected[tmpInfo.id] >= 1) {
453                     return tmpInfo;
454                 }
455             }
456             // 2-2. Choose the rpm of default profile
457             // TODO: should be supported
458
459             // 2-3. Choose recommends pkg (pkg-name or capability)
460             if (recommend) {
461                 for (let provide of provideList) {
462                     let provide_info = pkgInfo[provide.name];
463                     for (var reco of recommend) {
464                         // 2-3-1. select a pkg that is named
465                         if (reco.name == provide_info.name) {
466                             return provide_info;
467                         }
468                         // 2-3-2. select a pkg that provides
469                         for (var cap of provide_info['provides']) {
470                             if (reco.name == cap.name) {
471                                 return provide_info;
472                             }
473                         }
474                     }
475                 }
476             }
477             var maxVersion = null;
478             // 2-4. Select the latest version of rpm
479             _.forEach (provideList, function(provide) {
480                 var checkConflict = _checkConflicts(pkgInfo[provide.name]);
481                 if (checkConflict.result === false) {
482                     if (maxVersion) {
483                         var ret = _comapre_ver(maxVersion.data, provide.data)
484                         // version equals
485                         if (ret === 0) {
486                             // string compare
487                             if (maxVersion.name > provide.name) {
488                                 maxVersion = provide;
489                             }
490                         } else if (ret === -1) {
491                             // greater than maxVersion
492                             maxVersion = provide;
493                         }
494                     } else {
495                         maxVersion = provide;
496                     }
497                 }
498             });
499
500             // all of capability pkg are in conflict
501             if (_.isEmpty(maxVersion)) {
502                 return pkgInfo[provideList[0].name];
503             }
504
505             return pkgInfo[maxVersion.name];
506         }
507
508         function _checkConflicts(node) {
509             var ret = {'result': false, 'message': null};
510             // 1. Check whether node can be installed
511             if (!_.isEmpty(node.provides)) {
512                 _.forEach(node.provides, function(provide) {
513                     if (!_.isEmpty(conflicts[provide.name])) {
514                         _.forEach(conflicts[provide.name], function(conflict) {
515                             // compare version between conflict with provide
516                             if (!conflict.data.ver || _meetRequiredVersion(conflict.data, provide)) {
517                                 // package conflict
518                                 logger.info('[Conflict] The ' + node.name + ' conflict with ' + conflict.name);
519                                 ret.message = 'The ' + node.name + ' conflict with ' + conflict.name;
520                                 ret.result = true;
521                                 return false;
522                             }
523                         });
524                     }
525                     if (ret.result === true) {
526                         return false;
527                     }
528                 });
529             }
530             if (ret.result === false) {
531                 if (!_.isEmpty(conflicts[node.name])) {
532                     _.forEach(conflicts[node.name], function(conflict) {
533                         if (!conflict.data.ver || _meetRequiredVersion(conflict.data, node.version)) {
534                             // package conflict
535                             logger.info('[Conflict] The ' + node.name + ' conflict with ' + conflict.name);
536                             ret.message = 'The ' + node.name + ' conflict with ' + conflict.name;
537                             ret.result = true;
538                             return false;
539                         }
540                     });
541                 }
542             }
543
544             // 2. If the conflict package defined by node is installed
545             if (ret.result === false && !_.isEmpty(node.conflicts)) {
546                 _.forEach(node.conflicts, function(conflict) {
547                     if (_.has(provides, conflict.name)) {
548                         _.forEach(provides[conflict.name], function(provide) {
549                             if (pkgInfo[provide.name].checked === true) {
550                                 if (!conflict.ver || _meetRequiredVersion(conflict, provide.data)) {
551                                     // package conflicts
552                                     logger.info('[Conflict] The ' + node.name + ' conflict with ' + provide.name);
553                                     ret.message = 'The '+node.name+'(conflicts:'+conflict.name+') conflict with '+provide.name;
554                                     ret.result = true;
555                                     return false;
556                                 }
557                             }
558                         });
559                     } else if (_.has(pkgInfo, conflict.name)) {
560                         var pkg = pkgInfo[conflict.name];
561                         if (pkg.checked === true) {
562                             if (!conflict.ver || _meetRequiredVersion(conflict, pkg.version)) {
563                                 // package conflicts
564                                 logger.info('[Conflict] The ' + node.name + ' conflit with ' + pkgInfo.name);
565                                 ret.message = 'The '+node.name+'(conflicts:'+conflict.name+') conflict with '+pkg.name;
566                                 ret.result = true;
567                                 return false;
568                             }
569                         }
570                     }
571                     if (ret.result === true) {
572                         return false;
573                     }
574                 });
575             }
576             return ret;
577         }
578
579         function _addConflicts(node) {
580             if (!_.isEmpty(node.conflicts)) {
581                 _.forEach(node.conflicts, function(conflict) {
582                     // logger.info(node.name + ' add conflict rpm : ' + conflict.name);
583                     if (_.isEmpty(conflicts[conflict.name])) {
584                         conflicts[conflict.name] = [];
585                         conflicts[conflict.name].push({'name':node.name, 'data':conflict});
586                     } else {
587                         conflicts[conflict.name].push({'name':node.name, 'data':conflict});
588                     }
589                 });
590             }
591         }
592
593         function _analyzeDep(node) {
594             var pkgId = node.id;
595             number += 1;
596             selected[pkgId] = number;
597             min_num[pkgId] = number;
598             stack.push(node);
599
600             var dependents = {};
601             dependents[node.name] = node;
602
603             // Check for conflicts
604             var checkConflict = _checkConflicts(node)
605             if (checkConflict.result === true) {
606                 errorMsg = checkConflict.message;
607                 progressStatus = false;
608                 stack.pop();
609                 return null;
610             }
611
612             // add rpms into conflicts table.
613             _addConflicts(node);
614
615             // installation dependency analysis of package
616             _.forEach(['requires', 'recommends'], function(depTag) {
617                 if (_.has(node, depTag)) {
618                     _.forEach(node[depTag], function(require) {
619                         var choose = null;
620                         if (_.has(provides, require.name)) {
621                             var capList = provides[require.name];
622                             if (depTag == 'requires') {
623                                 choose = _select_rpm(capList, require, node['recommends']);
624                             } else {
625                                 choose = _select_rpm(capList, require);
626                             }
627                         } else if (_.has(files, require.name)) {
628                             choose = _select_files(files[require.name], require);
629                         } else if (_.has(pkgInfo, require.name)) {
630                             choose = pkgInfo[require.name];
631                         }
632
633                         if (depTag == 'recommends') {
634                             if (!choose || (_checkConflicts(choose)).result === true) {
635                                 logger.info(require.name + ' recommended by ' + node.name + ' is ignored for selection (Conflict)');
636                                 return; // continue
637                             }
638                         }
639
640                         if (choose) {
641                             if (selected[choose.id] === -1) {
642                                 var result = _analyzeDep(choose);
643                                 if (progressStatus === false) {
644                                     // Stop dependency analysis
645                                     return false;
646                                 }
647                                 _.forEach(result, function(value, key) {
648                                      if (!_.has(dependents, key)) {
649                                          dependents[key] = value;
650                                      }
651                                 });
652                                 min_num[pkgId] = Math.min(min_num[pkgId], min_num[choose.id])
653                             } else if (selected[choose.id] >= 1 && scc_id[choose.id] === 0) {
654                                 // cross edge that can not be ignored
655                                 min_num[pkgId] = Math.min(min_num[pkgId], min_num[choose.id])
656                             }
657
658                             // add forward/backward reference
659                             _createReference(node, choose);
660                         } else {
661                             // the capability does not exist. Stop dependency analysis
662                             progressStatus = false;
663                             errorMsg = 'The ' + require.name + ' needed by ' + node.name + ' does not exist. Please check the repository';
664                             logger.info('###' + errorMsg);
665                             // break forEach
666                             return false;
667                         }
668                     });
669                 }
670                 if (progressStatus === false) {
671                     // Stop dependency analysis
672                     return false;
673                 }
674             });
675
676             if (min_num[pkgId] === selected[pkgId]) {
677                 _makeSCC(pkgId);
678             }
679
680             return dependents;
681         }
682
683         var analyzeRet = _analyzeDep(checkNode);
684         if (progressStatus === false) {
685             return {'result': false, 'message': errorMsg};
686         }
687         return {'result': true, 'data': analyzeRet};
688     }
689
690     // analyze uncheck dependency
691     function _analyzeUncheckDependency(uncheckNode) {
692
693         function _checkCircularDependency(node) {
694             var groupId = node.group
695             var groupPkgList = groups[groupId];
696             var groupObj = {};
697
698             // Set object for group
699             _.forEach(groupPkgList, function(pkgName) {
700                 groupObj[pkgName] = null;
701             });
702
703             for(var i=0; i<groupPkgList.length; i++) {
704                 var pkg = _find(groupPkgList[i]);
705                 // the node is selfChecked (the root node ignores selfchecked)
706                 if (uncheckNode.id != pkg.id && pkg.selfChecked) {
707                     return false;
708                 }
709                 for(var b=0; b<pkg.backward.length; b++) {
710                     // If node is Referenced by another node (Not a node in the group),
711                     // unable to uncheck group nodes
712                     if (!_.has(groupObj, pkg.backward[b])) {
713                         return false;
714                     }
715                 }
716             }
717             // init visited obj
718             groupVisited[groupId] = {};
719             // Delete backward reference of group node
720             _.forEach(groupPkgList, function(pkgName) {
721                 var pkg = _find(pkgName);
722                 // backward ref.
723                 pkg.backward = null;
724                 // visited init
725                 groupVisited[groupId][pkg.name] = -1;
726             });
727             //logger.info('Group(' + groupId +  ') is uncheckable : ' + groupPkgList);
728             return true;
729         }
730
731         function _deleteConflictData(node) {
732             if (!_.isEmpty(node.conflicts)) {
733                 _.forEach(node.conflicts, function(conflict) {
734                     if (!_.isEmpty(conflicts[conflict.name])) {
735                         var cList = conflicts[conflict.name];
736                         for(var i=0; i<cList.length; i++) {
737                             if (cList[i].name === node.name) {
738                                 cList.splice(i, 1);
739                                 break;
740                             }
741                         }
742                     }
743                 });
744             }
745         }
746
747         function _analyzeUncheck(parent, node) {
748             if (!_.isEmpty(parent)) {
749                 // TODO: performance
750                 if (!_.isEmpty(node.backward)) {
751                     var bIndex = node.backward.indexOf(parent.name);
752                     if (bIndex > -1) {
753                         // TODO: performance
754                         // remove backward reference (parent)
755                         node.backward.splice(bIndex, 1);
756                     }
757                 }
758                 // selfCheck node is not unchecked
759                 if (node.selfChecked === true) {
760                     return null;
761                 }
762             }
763
764             var uncheckPkgs = null;
765             // Check that the selected node is uncheckable
766             if (!_.isEmpty(node.backward)) {
767                 // check circular dependency
768                 if (!node.group || !_checkCircularDependency(node)) {
769                     return null;
770                 }
771             }
772
773             // the selected node is uncheckable
774             uncheckPkgs = {};
775             uncheckPkgs[node.name] = node;
776             // uncheckable pkg of group
777             if (node.group && !_.isEmpty(groupVisited[node.group])) {
778                 groupVisited[node.group][node.name] = 1;
779             }
780
781             // if selected node has forward references
782             if (!_.isEmpty(node.forward)) {
783                 _.forEach(node.forward, function(fname) {
784                     var forwardNode = _find(fname);
785
786                     // If pkg has a circular dependency and is unchekcable,
787                     // circular dep. pkgs can only be visited once
788                     var fvisit = groupVisited[forwardNode.group];
789                     if (!_.isEmpty(fvisit) && fvisit[fname] === 1) {
790                         return; // continue;
791                     }
792
793                     var result = _analyzeUncheck(node, forwardNode);
794
795                     // updates pkgs for uncheck
796                     if (!_.isEmpty(result)) {
797                         _.forEach(result, function(value, key) {
798                             if (!_.has(uncheckPkgs, key)) {
799                                 uncheckPkgs[key] = value;
800                             }
801                         });
802                     }
803                 });
804                 // forward reference reset
805                 node.forward = null;
806                 node.group = null;
807             }
808             
809             // delete conflict data from conflicts table
810             _deleteConflictData(node);
811
812             return uncheckPkgs;
813         }
814
815         // { 'group_id' : {pkgName: -1, ... }, ... }
816         var groupVisited = {};
817         var uncheckPkgs = _analyzeUncheck(null, uncheckNode);
818
819         // delete groupId from groups
820         _.forEach(groupVisited, function(groupList, groupId) {
821             if (!_.isEmpty(groups[groupId])) {
822                 delete groups[groupId];
823             }
824         });
825
826         return uncheckPkgs;
827     }
828
829     /**
830      * Summary page
831      */
832     function _updateSummary() {
833         var pacakgeImageSize = $('#tic-package-image-size').empty();
834         var packageImageInstalledSize = $('#tic-package-image-installed-size').empty();
835         var packageListBadge = $('#tic-package-list-badge').empty();
836         var packageList = $('#tic-package-list').empty();
837
838         var list = _getChecked();
839         var count = _.size(list);
840
841         var imageSize = _.sumBy(list, function getImageSize(item) {
842             return _.toNumber(item.size || 0);
843         });
844         var imageInstalledSize = _.sumBy(list, function getImageInstalled(item) {
845             return _.toNumber(item.installed || 0);
846         });
847
848         if (_.isNumber(imageSize)) {
849             pacakgeImageSize.html(Util.bytesToSize(imageSize));
850         }
851         if (_.isNumber(imageInstalledSize)) {
852             packageImageInstalledSize.html(Util.bytesToSize(imageInstalledSize));
853         }
854         if (_.isNumber(count)) {
855             packageListBadge.html(count);
856         }
857         if (!_.isEmpty(list)) {
858             packageList.html(_.orderBy(_.map(list, 'name')).join('<br>'));
859         }
860
861         // check mic status to image creation
862         socket.emit(AppConfig.EVENT.SOCKET.MIC_AVAILABLE_FROM);
863     }
864
865     /**
866      * Treeview: A node is selected.
867      */
868     function _nodeSelected(event, node) {
869         var text = $('#tic-package-info-text').empty();
870         var version = $('#tic-package-info-version').empty();
871         var arch = $('#tic-package-info-arch').empty();
872         var size = $('#tic-package-info-size').empty();
873         var installedSize = $('#tic-package-info-installed-size').empty();
874         var summary = $('#tic-package-info-summary').empty();
875         var description = $('#tic-package-info-description').empty();
876
877         var pkg = _findFromNode(node);
878
879         if (!_.isEmpty(pkg)) {
880             if (!_.isEmpty(pkg.name)) {
881                 text.html(pkg.name);
882             }
883             if (!_.isEmpty(pkg.version)) {
884                 if (!_.isEmpty(pkg.version.rel)) {
885                     version.html(pkg.version.ver + '-' + pkg.version.rel);
886                 } else {
887                     version.html(pkg.version.ver);
888                 }
889             }
890             if (!_.isEmpty(pkg.arch)) {
891                 arch.html(pkg.arch);
892             }
893             if (!_.isEmpty(pkg.size)) {
894                 size.html(Util.bytesToSize(pkg.size));
895             }
896             if (!_.isEmpty(pkg.installed)) {
897                 installedSize.html(Util.bytesToSize(pkg.installed));
898             }
899             if (!_.isEmpty(pkg.summary)) {
900                 summary.html(pkg.summary);
901             }
902             if (!_.isEmpty(pkg.description)) {
903                 description.html(pkg.description);
904             }
905         } else {
906             // 'MISC' case
907             if (!_.isEmpty(node.text)) {
908                 text.html(node.text);
909             }
910         }
911     }
912
913     /**
914      * Treeview: A node is checked.
915      */
916     function _onNodeChecked(event, node) {
917         var startTS = performance.now();
918         var localNode = _findFromNode(node);
919
920         if (_.isEmpty(localNode)) {
921             // MISC is virtual pacakge.
922             return;
923         }
924         if (localNode.checked === true) {
925             $tree.treeview('uncheckNode', [node, { silent: false }]);
926             return;
927         }
928
929         logger.info('[Check] checked: ' + localNode.name);
930         _nodeSelected(event, node);
931
932         localNode.selfChecked = true;
933         var depResult = _analyzeInstallDependency(localNode)
934
935         // Removing all references because the node can not be checked
936         // Case: conflicts, rpm does not exist...
937         if (depResult.result === false) {
938             Util.showAlertDialog(depResult.message);
939             localNode.checked = true;
940             $tree.treeview('uncheckNode', [node, { silent: false }]);
941         }
942
943         var analyzeTS = performance.now();
944
945         // TODO: debug code (should be deleted)
946         // var tempcode = [];
947         // _.forEach(depPkg, function(value, key) { tempcode.push(value.name); });
948         // logger.info(localNode.name + ' install-dependency(' + tempcode.length +'): ' + tempcode);
949
950         var toggleNode = [];
951         if (!_.isEmpty(depResult.data)) {
952             _.forEach(depResult.data, function(value, key) {
953                 // update local data
954                 value.checked = true;
955                 // update treeview data
956                 _.forEach(value.view, function(node) {
957                     if (node.state.checked === false) {
958                         toggleNode.push(node);
959                     }
960                 });
961             });
962
963             $tree.treeview('checkNode', [toggleNode, { silent: true }]);
964             _updateSummary();
965
966             var endTS = performance.now();
967             logger.info('[Check] Total time: ' + (endTS - startTS) + 'ms');
968             logger.info('[Check] Analyze dep. time: ' + (analyzeTS - startTS) + 'ms');
969             logger.info('[Check] Update view time: ' + (endTS - analyzeTS) + 'ms');
970         }
971     }
972
973     /**
974      * Treeview: A node is unchecked.
975      */
976     function _onNodeUnchecked(event, node) {
977         var startTS = performance.now();
978         var localNode = _findFromNode(node);
979
980         if (_.isEmpty(localNode)) {
981             // MISC is virtual pacakge.
982             return;
983         }
984         if (localNode.checked === false) {
985             $tree.treeview('checkNode', [node, { silent: false }]);
986             return;
987         }
988
989         logger.info('unchecked: ' + node.text);
990
991         var uncheckPkgs = _analyzeUncheckDependency(localNode);
992
993         var analyzeTS = performance.now();
994
995         // TODO: debug code (should be deleted)
996         // var tempcode = [];
997         // _.forEach(uncheckPkgs, function(value, key) { tempcode.push(value.name); });
998         // logger.info(localNode.name + ' uncheck-dependency(' + tempcode.length +'): ' + tempcode);
999
1000         var toggleNode = [];
1001         if (!_.isEmpty(uncheckPkgs)) {
1002             _.forEach(uncheckPkgs, function(value, key) {
1003                 // update local data
1004                 value.checked = false;
1005                 // update treeview data
1006                 _.forEach(value.view, function(node) {
1007                     if (node.state.checked === true) {
1008                         toggleNode.push(node);
1009                     }
1010                 });
1011             });
1012
1013             $tree.treeview('uncheckNode', [toggleNode, { silent: true }]);
1014             localNode.selfChecked = false;
1015             _updateSummary();
1016         } else {
1017             Util.showAlertDialog('Could not uncheck the \'' +localNode.name + '\'' + '<br> because the \'' +  localNode.backward + '\' packages depends on it');
1018
1019             // selected node change to check state
1020             $tree.treeview('checkNode', [[node], { silent: true }]);
1021         }
1022
1023         var endTS = performance.now();
1024         logger.info('[Uncheck] Total time: ' + (endTS - startTS) + 'ms');
1025         logger.info('[Uncheck] Analyze dep. time: ' + (analyzeTS - startTS) + 'ms');
1026         logger.info('[Uncheck] Update view time: ' + (endTS - analyzeTS) + 'ms');
1027     }
1028
1029     /**
1030      * Treeview: Initialize tree data.
1031      */
1032     function updatePackageTree(rawData) {
1033         repos = rawData.repos;
1034         pkgInfo = rawData.data.packages
1035         provides = rawData.data.provides;
1036         files = rawData.data.files;
1037         conflicts = rawData.data.conflicts;
1038         groups = rawData.data.groups;
1039
1040         _.forEach(groups, function(value, key) {
1041             groupId += 1;
1042         })
1043
1044         return new Promise(function (resolve, reject) {
1045             function _onRendered(event, nodes) {
1046                 packages = _.values(nodes);
1047                 _.forEach(nodes, function(node, key) {
1048                      // add a reference variable for treeview
1049                      var pkg = _findFromNode(node);
1050                      if (pkg) {
1051                          if (_.isEmpty(pkg.view)) {
1052                              pkg.view = [node];
1053                          } else {
1054                              pkg.view.push(node);
1055                          }
1056                      }
1057                 });
1058                 _setDefaultPackage(rawData.defaultpackages);
1059                 _updateSummary();
1060                 // init filter and restore checkbox state
1061                 $('#tic-package-toolbar-source').click().click();
1062
1063                 resolve();
1064             }
1065
1066             function _onNodeUnselected(event, node) {
1067                 $('#tic-package-info-text').empty();
1068                 $('#tic-package-info-version').empty();
1069                 $('#tic-package-info-arch').empty();
1070                 $('#tic-package-info-size').empty();
1071                 $('#tic-package-info-installed-size').empty();
1072                 $('#tic-package-info-summary').empty();
1073                 $('#tic-package-info-description').empty();
1074             }
1075
1076             $tree.treeview({
1077                 data: rawData.view,
1078                 levels: 1,
1079                 showIcon: false,
1080                 showCheckbox: true,
1081                 onRendered: _onRendered,
1082                 onNodeSelected: _nodeSelected,
1083                 onNodeUnselected: _onNodeUnselected,
1084                 onNodeChecked: _onNodeChecked,
1085                 onNodeUnchecked: _onNodeUnchecked
1086             });
1087         });
1088     }
1089
1090     function _initToolbar() {
1091         function _filter(patternList) {
1092             var matchNodes = [];
1093             _.forEach(patternList, function (pattern) {
1094                 if (!_.isEmpty(_.trim(pattern))) {
1095                     var nodes = $tree.treeview('search', [ pattern, {
1096                         ignoreCase: true,     // case insensitive
1097                         exactMatch: false,    // like or equals
1098                         revealResults: true,  // reveal matching nodes
1099                     }]);
1100                     matchNodes = _.union(matchNodes, nodes);
1101                 }
1102             })
1103
1104             var filterNodes = [];
1105             _.forEach(defaultFilters, function (pattern) {
1106                 if (!_.isEmpty(_.trim(pattern))) {
1107                     var nodes = $tree.treeview('search', [ pattern, {
1108                         ignoreCase: true,     // case insensitive
1109                         exactMatch: false,    // like or equals
1110                         revealResults: true,  // reveal matching nodes
1111                     }]);
1112                     filterNodes = _.union(filterNodes, nodes);
1113                 }
1114             })
1115
1116             _.forEach(packages, function (node) {
1117                 node.$el.hide();
1118             });
1119
1120             if (!_.isEmpty(matchNodes)) {
1121                 _.forEach(matchNodes, function (node) {
1122                     node.$el.show();
1123                 });
1124             } else {
1125                 // empty input text
1126                 if (_.isEmpty(_.trim(_.take(patternList)))) {
1127                     _.forEach(packages, function (node) {
1128                         node.$el.show();
1129                     });
1130                 }
1131             }
1132
1133             _.forEach(filterNodes, function (node) {
1134                 node.$el.hide();
1135             });
1136             $tree.treeview('clearSearch');
1137         }
1138
1139         function _filterInput() {
1140             var filterText = $('#tic-package-toolbar-input').val();
1141             _filter([filterText]);
1142             $('#tic-package-toolbar-input-clear').toggleClass('hidden', _.isEmpty(filterText));
1143         }
1144         $('#tic-package-toolbar-input').on('keyup', _.debounce(_filterInput, 500));
1145
1146         function _inputClearBtnHandler() {
1147             $('#tic-package-toolbar-input').val('').focus();
1148             _filter([]);
1149             $(this).toggleClass('hidden', true);
1150         }
1151         $('#tic-package-toolbar-input-clear').on('click', _inputClearBtnHandler);
1152
1153         function _filterType() {
1154             var debug = $('#tic-package-toolbar-debug').is(':checked');
1155             if (debug) {
1156                 if (!_.includes(defaultFilters, '-debug')) {
1157                     defaultFilters.push('-debug');
1158                 }
1159             } else {
1160                 _.remove(defaultFilters, function(filter) {
1161                     return filter === '-debug';
1162                 });
1163             }
1164             var devel = $('#tic-package-toolbar-devel').is(':checked');
1165             if (devel) {
1166                 if (!_.includes(defaultFilters, '-devel')) {
1167                     defaultFilters.push('-devel');
1168                 }
1169             } else {
1170                 _.remove(defaultFilters, function(filter) {
1171                     return filter === '-devel';
1172                 });
1173             }
1174             var source = $('#tic-package-toolbar-source').is(':checked');
1175             if (source) {
1176                 if (!_.includes(defaultFilters, '-source')) {
1177                     defaultFilters.push('-source');
1178                 }
1179             } else {
1180                 _.remove(defaultFilters, function(filter) {
1181                     return filter === '-source';
1182                 });
1183             }
1184             _filter([$('#tic-package-toolbar-input').val()]);
1185         }
1186         // FIXME: bug for tree is not folding
1187         // $("#tic-package-toolbar-debug").on('click', _filterType);
1188         // $("#tic-package-toolbar-devel").on('click', _filterType);
1189         // $("#tic-package-toolbar-source").on('click', _filterType);
1190
1191         function _collapseAll() {
1192             $tree.treeview('collapseAll');
1193         }
1194         $('#tic-package-left-col-tree-toolbar-collapse-all').on('click', _collapseAll);
1195
1196         function _expandAll() {
1197             $tree.treeview('expandAll');
1198         }
1199         $('#tic-package-left-col-tree-toolbar-expand-all').on('click', _expandAll);
1200     }
1201
1202     function _initContextMenu() {
1203         function _uncheckAllBtnHandler() {
1204             // INFO: bug for state.checked = false in treeview objects
1205             $tree.treeview('checkAll', { silent: true });
1206             $tree.treeview('uncheckAll', { silent: true });
1207             _.forEach(pkgInfo, function(value, key) {
1208                 value.checked = false;
1209                 value.selfChecked = false;
1210                 value.forward = null;
1211                 value.backward = null;
1212                 value.group = null;
1213
1214                 // INFO: bug for state.checked = false in treeview objects
1215                 _.forEach(value.view, function(node) {
1216                     node.state.checked = false;
1217                 });
1218             });
1219             groups = {};
1220             conflicts = {};
1221             _updateSummary();
1222         }
1223         $tree.contextmenu({
1224             target: '#tic-package-context-menu',
1225             onItem: function (row, e) {
1226                 if (e.target.id === 'tic-package-context-menu-uncheck-all') {
1227                     _uncheckAllBtnHandler();
1228                 }
1229             }
1230         });
1231     }
1232
1233     function init() {
1234         logger.info('init');
1235
1236         _initToolbar();
1237         _initContextMenu();
1238
1239         // button - image creation
1240         $('#tic-package-create').on('click', onClickHandlerForImgCreationBtn);
1241
1242         Util.getAppConfig()
1243         .then(function (config) {
1244             AppConfig = config;
1245
1246             socket = Util.getWebSocket();
1247
1248             // Image Creation Button status
1249             socket.on(AppConfig.EVENT.SOCKET.MIC_AVAILABLE_TO, function (isAvailable) {
1250                 if (isAvailable && _.size(_getChecked()) > 0) {
1251                     $('#tic-package-create').toggleClass('disabled', false);
1252                 } else {
1253                     $('#tic-package-create').toggleClass('disabled', true);
1254                 }
1255             });
1256         });
1257     }
1258
1259     init();
1260
1261     return {
1262         /**
1263          * Initialize for treeview
1264          * @method updatePackageTree
1265          * @param {array} array of objects
1266          * @return Promise
1267          */
1268         updatePackageTree: updatePackageTree,
1269
1270         /**
1271          * Get checked package nodes
1272          * @method getCheckedPackages
1273          * @return {array} array of objects
1274          */
1275         getCheckedPackages: _getChecked
1276     }
1277 });