[CCSDK-28] populated the seed code for dgbuilder
[ccsdk/distribution.git] / dgbuilder / red / nodes / registry.js
1 /**
2  * Copyright 2014 IBM Corp.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  **/
16  
17 var util = require("util");
18 var when = require("when");
19 var whenNode = require('when/node');
20 var fs = require("fs");
21 var path = require("path");
22 var crypto = require("crypto"); 
23 var UglifyJS = require("uglify-js");
24
25 var events = require("../events");
26
27 var Node;
28 var settings;
29
30 function filterNodeInfo(n) {
31     var r = {
32         id: n.id,
33         name: n.name,
34         types: n.types,
35         enabled: n.enabled
36     }
37     if (n.hasOwnProperty("loaded")) {
38         r.loaded = n.loaded;
39     }
40     if (n.hasOwnProperty("module")) {
41         r.module = n.module;
42     }
43     if (n.hasOwnProperty("err")) {
44         r.err = n.err.toString();
45     }
46     return r;
47 }
48
49 var registry = (function() {
50     var nodeConfigCache = null;
51     var nodeConfigs = {};
52     var nodeList = [];
53     var nodeConstructors = {};
54     var nodeTypeToId = {};
55     var nodeModules = {};
56     
57     function saveNodeList() {
58         var nodeList = {};
59         
60         for (var i in nodeConfigs) {
61             if (nodeConfigs.hasOwnProperty(i)) {
62                 var nodeConfig = nodeConfigs[i];
63                 var n = filterNodeInfo(nodeConfig);
64                 n.file = nodeConfig.file;
65                 delete n.loaded;
66                 delete n.err;
67                 delete n.file;
68                 delete n.id;
69                 nodeList[i] = n;
70             }
71         }
72         if (settings.available()) {
73             return settings.set("nodes",nodeList);
74         } else {
75             return when.reject("Settings unavailable");
76         }
77     }
78     
79     return {
80         init: function() {
81             if (settings.available()) {
82                 nodeConfigs = settings.get("nodes")||{};
83                 // Restore the node id property to individual entries
84                 for (var id in nodeConfigs) {
85                     if (nodeConfigs.hasOwnProperty(id)) {
86                         nodeConfigs[id].id = id;
87                     }
88                 }
89             } else {
90                 nodeConfigs = {};
91             }
92             nodeModules = {};
93             nodeTypeToId = {};
94             nodeConstructors = {};
95             nodeList = [];
96             nodeConfigCache = null;
97         },
98         
99         addNodeSet: function(id,set) {
100             if (!set.err) {
101                 set.types.forEach(function(t) {
102                     nodeTypeToId[t] = id;
103                 });
104             }
105             
106             if (set.module) {
107                 nodeModules[set.module] = nodeModules[set.module]||{nodes:[]};
108                 nodeModules[set.module].nodes.push(id);
109             }
110             
111             nodeConfigs[id] = set;
112             nodeList.push(id);
113             nodeConfigCache = null;
114         },
115         removeNode: function(id) {
116             var config = nodeConfigs[id];
117             if (!config) {
118                 throw new Error("Unrecognised id: "+id);
119             }
120             delete nodeConfigs[id];
121             var i = nodeList.indexOf(id);
122             if (i > -1) {
123                 nodeList.splice(i,1);
124             }
125             config.types.forEach(function(t) {
126                 delete nodeConstructors[t];
127                 delete nodeTypeToId[t];
128             });
129             config.enabled = false;
130             config.loaded = false;
131             nodeConfigCache = null;
132             return filterNodeInfo(config);
133         },
134         removeModule: function(module) {
135             if (!settings.available()) {
136                 throw new Error("Settings unavailable");
137             }
138             var nodes = nodeModules[module];
139             if (!nodes) {
140                 throw new Error("Unrecognised module: "+module);
141             }
142             var infoList = [];
143             for (var i=0;i<nodes.nodes.length;i++) {
144                 infoList.push(registry.removeNode(nodes.nodes[i]));
145             }
146             delete nodeModules[module];
147             saveNodeList();
148             return infoList;
149         },
150         getNodeInfo: function(typeOrId) {
151             if (nodeTypeToId[typeOrId]) {
152                 return filterNodeInfo(nodeConfigs[nodeTypeToId[typeOrId]]);
153             } else if (nodeConfigs[typeOrId]) {
154                 return filterNodeInfo(nodeConfigs[typeOrId]);
155             }
156             return null;
157         },
158         getNodeList: function() {
159             var list = [];
160             for (var id in nodeConfigs) {
161                 if (nodeConfigs.hasOwnProperty(id)) {
162                     list.push(filterNodeInfo(nodeConfigs[id]))
163                 }
164             }
165             return list;
166         },
167         registerNodeConstructor: function(type,constructor) {
168             if (nodeConstructors[type]) {
169                 throw new Error(type+" already registered");
170             }
171             //TODO: Ensure type is known - but doing so will break some tests
172             //      that don't have a way to register a node template ahead
173             //      of registering the constructor
174             util.inherits(constructor,Node);
175             nodeConstructors[type] = constructor;
176             events.emit("type-registered",type);
177         },
178         
179         
180         /**
181          * Gets all of the node template configs
182          * @return all of the node templates in a single string
183          */
184         getAllNodeConfigs: function() {
185             if (!nodeConfigCache) {
186                 var result = "";
187                 var script = "";
188                 for (var i=0;i<nodeList.length;i++) {
189                     var config = nodeConfigs[nodeList[i]];
190                     if (config.enabled && !config.err) {
191                         result += config.config;
192                         script += config.script;
193                     }
194                 }
195                 if (script.length > 0) {
196                     result += '<script type="text/javascript">';
197                     result += UglifyJS.minify(script, {fromString: true}).code;
198                     result += '</script>';
199                 }
200                 nodeConfigCache = result;
201             }
202             return nodeConfigCache;
203         },
204         
205         getNodeConfig: function(id) {
206             var config = nodeConfigs[id];
207             if (config) {
208                 var result = config.config;
209                 if (config.script) {
210                     result += '<script type="text/javascript">'+config.script+'</script>';
211                 }
212                 return result;
213             } else {
214                 return null;
215             }
216         },
217         
218         getNodeConstructor: function(type) {
219             var config = nodeConfigs[nodeTypeToId[type]];
220             if (!config || (config.enabled && !config.err)) {
221                 return nodeConstructors[type];
222             }
223             return null;
224         },
225         
226         clear: function() {
227             nodeConfigCache = null;
228             nodeConfigs = {};
229             nodeList = [];
230             nodeConstructors = {};
231             nodeTypeToId = {};
232         },
233         
234         getTypeId: function(type) {
235             return nodeTypeToId[type];
236         },
237         
238         getModuleInfo: function(type) {
239             return nodeModules[type];
240         },
241         
242         enableNodeSet: function(id) {
243             if (!settings.available()) {
244                 throw new Error("Settings unavailable");
245             }
246             var config = nodeConfigs[id];
247             if (config) {
248                 delete config.err;
249                 config.enabled = true;
250                 if (!config.loaded) {
251                     // TODO: honour the promise this returns
252                     loadNodeModule(config);
253                 }
254                 nodeConfigCache = null;
255                 saveNodeList();
256             } else {
257                 throw new Error("Unrecognised id: "+id);
258             }
259             return filterNodeInfo(config);
260         },
261         
262         disableNodeSet: function(id) {
263             if (!settings.available()) {
264                 throw new Error("Settings unavailable");
265             }
266             var config = nodeConfigs[id];
267             if (config) {
268                 // TODO: persist setting
269                 config.enabled = false;
270                 nodeConfigCache = null;
271                 saveNodeList();
272             } else {
273                 throw new Error("Unrecognised id: "+id);
274             }
275             return filterNodeInfo(config);
276         },
277         
278         saveNodeList: saveNodeList,
279         
280         cleanNodeList: function() {
281             var removed = false;
282             for (var id in nodeConfigs) {
283                 if (nodeConfigs.hasOwnProperty(id)) {
284                     if (nodeConfigs[id].module && !nodeModules[nodeConfigs[id].module]) {
285                         registry.removeNode(id);
286                         removed = true;
287                     }
288                 }
289             }
290             if (removed) {
291                 saveNodeList();
292             }
293         }
294     }
295 })();
296
297
298
299 function init(_settings) {
300     Node = require("./Node");
301     settings = _settings;
302     registry.init();
303 }
304
305 /**
306  * Synchronously walks the directory looking for node files.
307  * Emits 'node-icon-dir' events for an icon dirs found
308  * @param dir the directory to search
309  * @return an array of fully-qualified paths to .js files
310  */
311 function getNodeFiles(dir) {
312     var result = [];
313     var files = [];
314     try {
315         files = fs.readdirSync(dir);
316     } catch(err) {
317         return result;
318     }
319     files.sort();
320     files.forEach(function(fn) {
321         var stats = fs.statSync(path.join(dir,fn));
322         if (stats.isFile()) {
323             if (/\.js$/.test(fn)) {
324                 var valid = true;
325                 if (settings.nodesExcludes) {
326                     for (var i=0;i<settings.nodesExcludes.length;i++) {
327                         if (settings.nodesExcludes[i] == fn) {
328                             valid = false;
329                             break;
330                         }
331                     }
332                 }
333                 valid = valid && fs.existsSync(path.join(dir,fn.replace(/\.js$/,".html")))
334                 
335                 if (valid) {
336                     result.push(path.join(dir,fn));
337                 }
338             }
339         } else if (stats.isDirectory()) {
340             // Ignore /.dirs/, /lib/ /node_modules/ 
341             if (!/^(\..*|lib|icons|node_modules|test)$/.test(fn)) {
342                 result = result.concat(getNodeFiles(path.join(dir,fn)));
343             } else if (fn === "icons") {
344                 events.emit("node-icon-dir",path.join(dir,fn));
345             }
346         }
347     });
348     return result;
349 }
350
351 /**
352  * Scans the node_modules path for nodes
353  * @param moduleName the name of the module to be found
354  * @return a list of node modules: {dir,package}
355  */
356 function scanTreeForNodesModules(moduleName) {
357     var dir = __dirname+"/../../nodes";
358     var results = [];
359     var up = path.resolve(path.join(dir,".."));
360     while (up !== dir) {
361         var pm = path.join(dir,"node_modules");
362         try {
363             var files = fs.readdirSync(pm);
364             for (var i=0;i<files.length;i++) {
365                 var fn = files[i];
366                 if (!registry.getModuleInfo(fn)) {
367                     if (!moduleName || fn == moduleName) {
368                         var pkgfn = path.join(pm,fn,"package.json");
369                         try {
370                             var pkg = require(pkgfn);
371                             if (pkg['node-red']) {
372                                 var moduleDir = path.join(pm,fn);
373                                 results.push({dir:moduleDir,package:pkg});
374                             }
375                         } catch(err) {
376                             if (err.code != "MODULE_NOT_FOUND") {
377                                 // TODO: handle unexpected error
378                             }
379                         }
380                         if (fn == moduleName) {
381                             break;
382                         }
383                     }
384                 }
385             }
386         } catch(err) {
387         }
388         
389         dir = up;
390         up = path.resolve(path.join(dir,".."));
391     }
392     return results;
393 }
394
395 /**
396  * Loads the nodes provided in an npm package.
397  * @param moduleDir the root directory of the package
398  * @param pkg the module's package.json object
399  */
400 function loadNodesFromModule(moduleDir,pkg) {
401     var nodes = pkg['node-red'].nodes||{};
402     var results = [];
403     var iconDirs = [];
404     for (var n in nodes) {
405         if (nodes.hasOwnProperty(n)) {
406             var file = path.join(moduleDir,nodes[n]);
407             try {
408                 results.push(loadNodeConfig(file,pkg.name,n));
409             } catch(err) {
410             }
411             var iconDir = path.join(moduleDir,path.dirname(nodes[n]),"icons");
412             if (iconDirs.indexOf(iconDir) == -1) {
413                 if (fs.existsSync(iconDir)) {
414                     events.emit("node-icon-dir",iconDir);
415                     iconDirs.push(iconDir);
416                 }
417             }
418         }
419     }
420     return results;
421 }
422
423
424 /**
425  * Loads a node's configuration
426  * @param file the fully qualified path of the node's .js file
427  * @param name the name of the node
428  * @return the node object
429  *         {
430  *            id: a unqiue id for the node file
431  *            name: the name of the node file, or label from the npm module
432  *            file: the fully qualified path to the node's .js file
433  *            template: the fully qualified path to the node's .html file
434  *            config: the non-script parts of the node's .html file
435  *            script: the script part of the node's .html file
436  *            types: an array of node type names in this file
437  *         }
438  */
439 function loadNodeConfig(file,module,name) {
440     var id = crypto.createHash('sha1').update(file).digest("hex");
441     if (module && name) {
442         var newid = crypto.createHash('sha1').update(module+":"+name).digest("hex");
443         var existingInfo = registry.getNodeInfo(id);
444         if (existingInfo) {
445             // For a brief period, id for modules were calculated incorrectly.
446             // To prevent false-duplicates, this removes the old id entry
447             registry.removeNode(id);
448             registry.saveNodeList();
449         }
450         id = newid;
451
452     }
453     var info = registry.getNodeInfo(id);
454     
455     var isEnabled = true;
456
457     if (info) {
458         if (info.hasOwnProperty("loaded")) {
459             throw new Error(file+" already loaded");
460         }
461         isEnabled = info.enabled;
462     }
463     
464     var node = {
465         id: id,
466         file: file,
467         template: file.replace(/\.js$/,".html"),
468         enabled: isEnabled,
469         loaded:false
470     }
471     
472     if (module) {
473         node.name = module+":"+name;
474         node.module = module;
475     } else {
476         node.name = path.basename(file)
477     }
478     try {
479         var content = fs.readFileSync(node.template,'utf8');
480         
481         var types = [];
482         
483         var regExp = /<script ([^>]*)data-template-name=['"]([^'"]*)['"]/gi;
484         var match = null;
485         
486         while((match = regExp.exec(content)) !== null) {
487             types.push(match[2]);
488         }
489         node.types = types;
490         node.config = content;
491         
492         // TODO: parse out the javascript portion of the template
493         node.script = "";
494         
495         for (var i=0;i<node.types.length;i++) {
496             if (registry.getTypeId(node.types[i])) {
497                 node.err = node.types[i]+" already registered";
498                 break;
499             }
500         }
501     } catch(err) {
502         node.types = [];
503         if (err.code === 'ENOENT') {
504             node.err = "Error: "+file+" does not exist";
505         } else {
506             node.err = err.toString();
507         }
508     }
509     registry.addNodeSet(id,node);
510     return node;
511 }
512
513 /**
514  * Loads all palette nodes
515  * @param defaultNodesDir optional parameter, when set, it overrides the default
516  *                        location of nodeFiles - used by the tests
517  * @return a promise that resolves on completion of loading
518  */
519 function load(defaultNodesDir,disableNodePathScan) {
520     return when.promise(function(resolve,reject) {
521         // Find all of the nodes to load
522         var nodeFiles;
523         if(defaultNodesDir) {
524             nodeFiles = getNodeFiles(path.resolve(defaultNodesDir));
525         } else {
526             nodeFiles = getNodeFiles(__dirname+"/../../nodes");
527         }
528         
529         if (settings.nodesDir) {
530             var dir = settings.nodesDir;
531             if (typeof settings.nodesDir == "string") {
532                 dir = [dir];
533             }
534             for (var i=0;i<dir.length;i++) {
535                 nodeFiles = nodeFiles.concat(getNodeFiles(dir[i]));
536             }
537         }
538         var nodes = [];
539         nodeFiles.forEach(function(file) {
540             try {
541                 nodes.push(loadNodeConfig(file));
542             } catch(err) {
543                 // 
544             }
545         });
546         
547         // TODO: disabling npm module loading if defaultNodesDir set
548         //       This indicates a test is being run - don't want to pick up
549         //       unexpected nodes.
550         //       Urgh.
551         if (!disableNodePathScan) {
552             // Find all of the modules containing nodes
553             var moduleFiles = scanTreeForNodesModules();
554             moduleFiles.forEach(function(moduleFile) {
555                 nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package));
556             });
557         }
558         var promises = [];
559         nodes.forEach(function(node) {
560             if (!node.err) {
561                 promises.push(loadNodeModule(node));
562             }
563         });
564         
565         //resolve([]);
566         when.settle(promises).then(function(results) {
567             // Trigger a load of the configs to get it precached
568             registry.getAllNodeConfigs();
569             
570             if (settings.available()) {
571                 resolve(registry.saveNodeList());
572             } else {
573                 resolve();
574             }
575         }); 
576     });
577 }
578
579 /**
580  * Loads the specified node into the runtime
581  * @param node a node info object - see loadNodeConfig
582  * @return a promise that resolves to an update node info object. The object
583  *         has the following properties added:
584  *            err: any error encountered whilst loading the node
585  *            
586  */
587 function loadNodeModule(node) {
588     var nodeDir = path.dirname(node.file);
589     var nodeFn = path.basename(node.file);
590     if (!node.enabled) {
591         return when.resolve(node);
592     }
593     try {
594         var loadPromise = null;
595         var r = require(node.file);
596         if (typeof r === "function") {
597             var promise = r(require('../red'));
598             if (promise != null && typeof promise.then === "function") {
599                 loadPromise = promise.then(function() {
600                     node.enabled = true;
601                     node.loaded = true;
602                     return node;
603                 }).otherwise(function(err) {
604                     node.err = err;
605                     return node;
606                 });
607             }
608         }
609         if (loadPromise == null) {
610             node.enabled = true;
611             node.loaded = true;
612             loadPromise = when.resolve(node);
613         }
614         return loadPromise;
615     } catch(err) {
616         node.err = err;
617         return when.resolve(node);
618     }
619 }
620
621 function loadNodeList(nodes) {
622     var promises = [];
623     nodes.forEach(function(node) {
624         if (!node.err) {
625             promises.push(loadNodeModule(node));
626         } else {
627             promises.push(node);
628         }
629     });
630     
631     return when.settle(promises).then(function(results) {
632         return registry.saveNodeList().then(function() {
633             var list = results.map(function(r) {
634                 return filterNodeInfo(r.value);
635             });
636             return list;
637         });
638     });
639 }
640
641 function addNode(file) {
642     if (!settings.available()) {
643         throw new Error("Settings unavailable");
644     }
645     var nodes = [];
646     try { 
647         nodes.push(loadNodeConfig(file));
648     } catch(err) {
649         return when.reject(err);
650     }
651     return loadNodeList(nodes);
652 }
653
654 function addModule(module) {
655     if (!settings.available()) {
656         throw new Error("Settings unavailable");
657     }
658     var nodes = [];
659     if (registry.getModuleInfo(module)) {
660         return when.reject(new Error("Module already loaded"));
661     }
662     var moduleFiles = scanTreeForNodesModules(module);
663     if (moduleFiles.length === 0) {
664         var err = new Error("Cannot find module '" + module + "'");
665         err.code = 'MODULE_NOT_FOUND';
666         return when.reject(err);
667     }
668     moduleFiles.forEach(function(moduleFile) {
669         nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package));
670     });
671     return loadNodeList(nodes);
672 }
673
674 module.exports = {
675     init:init,
676     load:load,
677     clear: registry.clear,
678     registerType: registry.registerNodeConstructor,
679     get: registry.getNodeConstructor,
680     getNodeInfo: registry.getNodeInfo,
681     getNodeModuleInfo: registry.getModuleInfo,
682     getNodeList: registry.getNodeList,
683     getNodeConfigs: registry.getAllNodeConfigs,
684     getNodeConfig: registry.getNodeConfig,
685     addNode: addNode,
686     removeNode: registry.removeNode,
687     enableNode: registry.enableNodeSet,
688     disableNode: registry.disableNodeSet,
689     
690     addModule: addModule,
691     removeModule: registry.removeModule,
692     cleanNodeList: registry.cleanNodeList
693 }