Rain Documentation

cache

../lib/cache.js

Caching module. Works in-node-memory (simple JavaScript object) and using redis.

  • type: String

  • api: public

var mod_promise         = require('promised-io')
    , mod_resources     = require('./resources.js')
    , mod_redback       = null
    , redbackClient     = null
    , config            = null
    , logger            = require('./logger.js').getLogger('Cache', require('./logger.js').Logger.SEVERITY.ERROR)
    , cache             = null

// var _cache = {};
// var cache = {
//     add : function (key, value, cb) {
//         if (typeof _cache[key] === 'undefined') {
//             _cache[key] = value;
//         }
//         cb();
//     }
//     , get : function (key, cb) {
//         cb(null, _cache[key] ? _cache[key] : null);
//     }
//     , set : function (key, value, exp, cb) {
//         _cache[key] = value;
//         cb();
//     } 
//     ,exists : function (key, cb) { 
//         cb(typeof _cache[key] !== 'undefined');
//     }
// }

function configure(c) {
    config = c;
    if (config && config.caching && config.caching.resources) {
        logger.debug('activate caching');
        mod_redback = require('redback');
        redbackClient = mod_redback.createClient();
        cache = redbackClient.createCache('rain');
    }

}

function toCache(url, data) {
    if (cache) {
        logger.debug('write to cache; url: ' + url);
        // [TBD] error handling
        cache.set(url, data, config.caching.ttl, function () {
            //logger.debug('written to cache');
        });
    }
}

function fromCache(url) {
    var defer = mod_promise.defer()
        , cr

    if (cache) {
        cache.exists(url, function (err, exists) {
            // [TBD] error handling
            if (err) {
                throw new Error(err.toString());
                defer.resolve(null);
            }
            if (exists) {
                cache.get(url, function (err, value) {
                    if (err) {
                        throw new Error(err);
                    } else {
                        logger.debug('got from cache; url: ' + url);
                        cr = new mod_resources.Resource(url, value);
                        defer.resolve(cr);
                    }  
                });
            } else {
                logger.debug('not found in cache; url: ' + url);
                defer.resolve(null);
            }
        });
    } else {
        defer.resolve(null);
    }
    return defer;
}

exports.fromCache   = fromCache;
exports.toCache     = toCache;
exports.configure   = configure;

cssnormalizer

../lib/cssnormalizer.js

This module normalizes URLs in CSS files provided by modules to absolute paths that are valid on an individual module host.

var mod_path			= require('path')
	mod_assert			= require('assert')
 	 , c 				= console.log

Normalizes a URL in a CSS file. Does not change fully-qualified http:// URLs.

  • param: String data CSS data

  • param: String path root path

  • param: String orighost URL of old source host

  • param: String newhost URL of new source host

  • return: String normalized CSS data

  • publi: c

function normalize(data, path, orighost, newhost) {
	var data = data.replace(/url\(['"]?([^'"\)']+)['"]?\)?/mg, function () {
		var p;
		if (arguments[1].indexOf('http://') === 0) { return arguments[1]; }
		p = mod_path.join(path, arguments[1]); 
		if (orighost !== newhost) { p = newhost + p; }
		p = 'url(\"' + p + '\")';
		return p;
 	});
 	return data;
}

exports.normalize = normalize;

logger

../lib/logger.js

Logger module. Has appenders for console and HTTP.

function getLogger (name, level) {
    var l = typeof level !== 'undefined' ? level : Logger.SEVERITY.INFO
    return new Logger(name, l);
}

function Logger(name, level) {
    if ('undefined' === typeof name) { name = '' }
    this.appender = [ new ConsoleAppender() ]; // yes, I know... 
    this.name = name;
    this.level = level;
}
Logger.SEVERITY = {
    INFO  : 5,
    DEBUG : 4,
    WARN  : 3,
    ERROR : 2
}

//
// [TBD] add argument style and join 
//
Logger.prototype.info = function (msg) {
  this._log(msg, Logger.SEVERITY.INFO);
}
Logger.prototype.debug = function (msg) {
  this._log(msg, Logger.SEVERITY.DEBUG);
}
Logger.prototype.warn = function (msg) {
  this._log(msg, Logger.SEVERITY.WARN);
}
Logger.prototype.error = function (msg) {
  this._log(msg, Logger.SEVERITY.ERROR);
}
Logger.prototype._log = function (msg, level) {
  if (level > this.level) { return; }
  for (var i = this.appender.length; --i >= 0;) {
    this.appender[i].append(msg, this.name, level);
  }
}

function ConsoleAppender () {
}
ConsoleAppender.prototype.append = function (msg, logger, level) {
    var col = level == 2 ? '\033[31m' : '\033[32m',
        msg = logger != '' ? (col + logger + ':\033[39m ' + msg ) : msg
    // there's no console.debug, map debug to console.info
    var lvls = {'5':'info', '4':'info', '3':'warn', '2' :'error'};
    console[lvls[level]](msg);
}

function HttpLogHostAppender (url) {  
    var opts = parseUri(url);
    this.options = {
        host: opts.host,
        port: opts.port,
        path: opts.path,
        method: 'POST',
        headers : {
            "Content-Type" : "application/json"
        }
    }; 
}
HttpLogHostAppender.prototype.append = function (msg, logger, level) {
    var req = http.request(this.options, function(res) {
      res.setEncoding('utf8');
      res.on('data', function (chunk) {});
    });
    var data = {};
    data.msg = msg;
    data.origin = logger;
    data.timestamp = new Date().getTime();
    try {
        req.write(JSON.stringify(data));    
    } catch (error) {
        // JSON.stringify does not allow circular references, so we need to 
        // catch that here
    }
    req.end();
}

exports.getLogger   = getLogger;
exports.Logger      = Logger;

modules

../lib/modules.js

  • type: String

  • api: public

var mod_sys                 = require('sys')
    , mod_path              = require('path')
    , mod_fs                = require('fs')
    , mod_promise           = require('promised-io')
    , mod_jsdom             = require('jsdom')
    , mod_url               = require('url')
    , mod_querystring       = require('querystring')
    , mod_renderer          = require('./renderer.js')
    , mod_resourcemanager   = require('./resourcemanager.js')
    , tagmanager            = null
    , config                = null
    , taglibrary            = null
    , moduleRootFolder      = mod_path.join(__dirname, '..', 'modules')
    , logger                = require('./logger.js').getLogger('Modules')

function configure(c, t) {
    config = c;
    tagmanager = t;
}

function handleControllerRequest (req, res, next) {
    logger.debug('handleControllerRequest ' + req.url);
    var modulename = req.params[0]
        , method = req.params[1]
        , mp = mod_path.join(moduleRootFolder, 'modules', modulename, 'main.js')

    mod_path.exists(mp, function (exists) {
        if (exists) {
            var module = require(mp);
            if (module[method]) {
                module[method](req, res).then(function (ret) {
                    res.end(ret.data);
                });
            } else {
                res.writeHead(404, { 'Content-Type' : 'text/plain'} );
                res.end('method not available');
            }   
        } else {
            res.writeHead(404, { 'Content-Type' : 'text/plain'} );
            res.end('unknown module ' + modulename);
        }
    });
}

function handleScriptRequest (req, res, next) {
    var modulename = req.params[0]
        , path = req.params[1]
        , mp = 'file://' + mod_path.join(moduleRootFolder, modulename, 'htdocs', path);
    logger.debug('handleScriptRequest ' + mp);
    res.setHeader('Content-Type', 'text/javascript');
    mod_resourcemanager.getResource(mp).then(function (resource) {
        res.end(resource.data.toString());
    });
}

function handleViewRequest (req, res, next) {
    var query = mod_querystring.parse(mod_url.parse(req.url).query)
        , mode = query.type
        , baseurl = req.url.substring(0, req.url.lastIndexOf('/'))
        , modulename = req.params[0]
        , viewname = req.params[1]
    logger.debug('handleViewRequest, module: ' + req.params[0] + ', view: ' + req.params[1]);

    var r = new mod_renderer.Renderer(tagmanager);
    console.time('render')
    r.render(getViewUrl(modulename, viewname), mode).then(function (doc) {
        console.timeEnd('render');
        res.end(doc);
        
    });
}


const DEFAULT_VIEW = 'main.html';

Gets an absolute file URL of a module.

  • param: String moduleId unique module id

  • param: String viewname view name, may be a partial path

  • return: String

function getViewUrl(moduleid, viewname) {
    return 'file://' + require('path')
            .join(__dirname
                    , '..'
                    , 'modules'
                    , moduleid
                    , 'htdocs'
                    , viewname ? viewname : DEFAULT_VIEW
            );
}   

// [TBD] will ask the module factory
function getModule (modulename) {
    return true;
}

exports.handleControllerRequest = handleControllerRequest;
exports.handleScriptRequest     = handleScriptRequest;
exports.handleViewRequest       = handleViewRequest;
exports.configure               = configure;
exports.getViewUrl              = getViewUrl;
exports.getModule               = getModule;

redisclient

../lib/redisclient.js

Manages the connection to redis db.

var redback = require('redback')
    , redbackClient = redback.createClient()
	
//cache = redbackClient.createCache('softporn');
//console.log(require('sys').inspect(redbackClient));
console.log('redis client started');

var channel = redbackClient.createChannel(redbackClient);

channel.publish('foobar', function () {
	console.log('published');
});

channel.subscribe('foobar', function (data) {
	console.log('received ' + data)
})

channel.on('message', function (msg) {
	console.log('message!! ' + msg);
            // assert.equal('foo', msg);
            // if (msg != 'foo') {
            //     assert.ok(false);
            // }
            // received = true;
        });

renderer

../lib/renderer.js

Here da magic.

var mod_xml                 = require('node-xml')
    , mod_promise           = require('promised-io')
    , sys                   = require('sys')
    , logger                = require('./logger.js').getLogger('TagManager')
    , mod_tagmanager        = require('./tagmanager.js')
    , mod_resourcemanager   = require('./resourcemanager.js')
    , mod_resources         = require('./resources.js')
    , mod_path              = require('path')
    , mod_modules           = require('./modules.js')    

    , c     = console.log
    , i     = sys.inspect
    , mr    = function () { return Math.random()*0; }

function Renderer(tagmgr) {
    if (!tagmgr) { throw new Error('tagmanager required'); }
    this.tagmanager = tagmgr
    this.renderResources = [];
}

The main workhorse function.

  • param: String View template absolute file:// URL

  • param: String Render mode, 'json' or 'html'

  • returns: Promise

Renderer.prototype.render = function (url, mode) {
    var self = this;
    return this._render(url).then(function (renderres) {
       c(self.renderResources.length);
        var deps = self.calcDependencies(renderres)
            , out = self[mode!=='json' ? 'outputHtml' : 'outputJson'](renderres, deps.css, deps.scripts, deps.locales);
        return out;
    } );
}

Renderer.prototype.calcDependencies = function (renderres) {        
    var self = this
        , css = []
        , scripts = []
        , locales = []
        , regex = new RegExp('file://' + mod_path.join(__dirname, '..'));

        this.renderResources.reverse().forEach(function (dep) {    
            c('................... ' + dep.url)
            path = dep.url.replace(regex, "")
                          .replace(/htdocs.*$/, '');
            Array.prototype.push.apply(css, dep.cssdeps.map(function (item) {
                return mod_path.join(path, item);
            }));
            Array.prototype.push.apply(scripts, dep.scriptdeps.map(function (item) {
                return mod_path.join(path,  item);
            }));
            Array.prototype.push.apply(locales, dep.localedeps.map(function (item) {
                return mod_path.join(path,  'i18n', item);
            }));
        });

        this.renderResources.reverse().forEach(function (dep) {
            c(dep.url);
        });

        return { 'css' : css, 'scripts' : scripts, 'locales' : locales };
    }

// [TBD] domain of ResourceService
const RESOURCE_LOADER_URLPATH = "/resources?files";

Transforms a RenderResource into HTML output format. This includes all dependencies of a view that are rendered into the output by using require.js function calls to have additional dependencies loaded by the web client.

  • param: Resource resource RenderedViewResource

  • param: String[] css URL to CSS dependency

  • param: String[] scripts URL to JavaScript dependency

  • param: String[] locales URL to locale dependency

Renderer.prototype.outputHtml = function (resource, css, scripts, locales) {
    var doc = resource.data.toString();

    var markup = [];
    
    // add CSS required by requested view
    // [TBD] the resource service should know how to create links to it, not this module. 
    if (css && css.length) {
      markup.push('<link rel="stylesheet" type="text/css" href="'
                  , RESOURCE_LOADER_URLPATH, '=',css.join(';'), '"/>\n'); 
    }
    // add JavaScript required by requested view
    // [TBD] the resource service should know how to create links to it, not this module. 
    if (scripts &amp;&amp; scripts.length) {
      markup.push('<script type="application/javascript" src="', RESOURCE_LOADER_URLPATH, '='
                  , scripts.join(';'), '"></script>\n');
    }

    locales.forEach(function (locale) {
        c('l ' + locale);
    });

    if (resource.elements.length &gt; 0) {
        markup.push('\n<script type="application/javascript">\n');

        // this.renderResources.reverse().forEach(function (dep) { 
        //     markup.push('console.log("'+dep.url+'");');
        // });
        resource.elements.forEach(function (elem) {
            var im = elem.instanceId ? ',"text!/instances/' + elem.instanceId + '.js"' : '';
            var l = elem.locale
            markup.push('\nrequire(["/modules/', elem.moduleId, '/client.js"'
                          , ', "text!/modules/', elem.moduleId, '/main.html?type=json"'
                          , im
                          , ', "text!/modules/', elem.moduleId, '/locales/de_DE.xml"'
                          , '], '
                          , ' function (module, template, instance, localefile) { module.initView("'
                          , elem.id, '", template, instance, localefile) } );'
            );
        });
        markup.push('\n</script>\n');
    }
    
    // this, erm, could be done more elegantly. but it frakking works for now ^^ 
    var idx = doc.indexOf('</head>');
    doc = doc.substring(0, idx) + markup.join('') + doc.substring(idx, doc.length);

    return doc;
}

Renders a resource as JSON. Currently mixes up partials and JSON output, which should not be the same.

  • param: RenderedViewResource resource Resource to render

  • param: String[] css URL to CSS dependency

  • param: String[] scripts URL to JavaScript dependency

  • param: String[] locales URL to locale dependency

Renderer.prototype.outputJson = function (resource, css, scripts, locales) {
    var body = resource.data.toString().match(/(<body[^>]*>)([\s\S]*)(<\/body&gt;)/mi)[2];
    var obj = { 
      &quot;resources&quot; : {
        &quot;css&quot;       : css
        , &quot;script&quot;  : scripts
        , &quot;locales&quot; : locales
      }
      , &quot;content&quot; : body
    };
    return JSON.stringify(obj);
}

Renderer.prototype._render = function (url) {
    var p = new mod_promise.Promise()
        , self = this
    c('render ' + url);

    self.loadResource(url).then(function (resource){
        if (!resource instanceof mod_resources.Resource) { throw new Error('wrong type'); }
        self.parse(resource).then(function (renderres) { 
            if (!renderres instanceof mod_resources.RenderedViewResource) { throw new Error('wrong type'); }
            self.renderResources.push(renderres);
            c('pushing ' + renderres.elements)
            var r = []
                , depsdone = {};
            if (renderres &amp;&amp; renderres.elements) {
                renderres.elements.forEach(function (item) {
                    debugger;
                    if (mod_modules.getModule(item.moduleId)) {
                        var viewurl = mod_modules.getViewUrl(item.moduleId, 'main.html');
                        if (!depsdone[viewurl]) {
                            r.push(self._render(viewurl));  
                        } else { }
                        depsdone[viewurl] = true;
                    } else {
                        throw new Error('module not found');
                    }
                });
                mod_promise.all(r).then(function (vals) {
                    c('resolved all sub-renders ' + url);
                    p.resolve(renderres);
                });
            } else {
                p.resolve(renderres);
            }
        });
    });
    return p;
}

Renderer.prototype.parse = function (resource) {
    return parseHtmlView(resource, this.tagmanager);
}

Renderer.prototype.loadResource = function (url) {
    return mod_resourcemanager.getResource(url);
}

exports.Renderer = Renderer;

function parseHtmlView (resource, tagmanager) {
    var defer               = new mod_promise.defer()
        , url               = resource.url
        , parser            = new mod_xml.SaxParser(new DocParser())

        // these are filled by the parser and returned to the caller 
        , elements          = [] // web components used by view, in parse order
        , outputBuffer      = []
        , cssresources      = [] // css resources used by view, in document order
        , scriptresources   = [] // javasript resources used by view, in document order
        , textresources     = [] // javasript resources used by view, in document order

    parser.parseString(resource.data.toString());
    return defer.promise;

    function DocParser() {
        var tagRemoved = false;

        return function (cb) {
            cb.onStartDocument(function() {
                //console.log('onStartDocument');  
            });

            cb.onEndDocument(function() {
                console.log(elements);
                defer.resolve(new mod_resources.RenderedViewResource(url
                                                                    , new Buffer(outputBuffer.join(''))
                                                                    , elements
                                                                    , cssresources
                                                                    , scriptresources
                                                                    , textresources)
                );
            });

            cb.onStartElementNS(function(elem, attrs, prefix, uri, namespaces) {
                //sys.puts("=====> Started: " + elem + " uri="+uri +" (Attributes: " + JSON.stringify(attrs) + " )");
                var instanceid
                    , viewname

                //
                // a document should not be transformed in parser. 
                // reminder to refactor to new render transformer
                // 
                //logger.debug(uri);
                // [TBD] should do namespace check
                if (elem === 'link') {
                    attrs.forEach(function (item) {
                        if (item[0] === 'href') {
                            cssresources.push(item[1]);
                            tagRemoved = true;
                        }
                    });
                } else if (elem == 'script') {
                    attrs.forEach(function (item) {
                        if (item[0] === 'src') {
                            scriptresources.push(item[1]);
                            tagRemoved = true;
                        }
                    });
                } else if (elem == 'text' &amp;&amp; prefix === 'tx') {
                    attrs.forEach(function (item) {
                        if (item[0] === 'key') {
                            textresources.push(item[1]);
                        }
                    });
                } else {
                    var tag = tagmanager.getTag(elem, attrs, prefix, uri, namespaces);
                    if (tag !== null) {
                        //logger.debug('found tag ' + tag.selector);
                        for (var i = 0; i &lt; attrs.length; i++) {
                            if (attrs[i][0] === 'instanceid') {
                                instanceid = attrs[i][1];
                                break;
                            }
                            if (attrs[i][0] === 'view') {
                                viewname = attrs[i][1];
                                break;
                            }
                        }
                        var eid = addId(attrs);
                        var elemResource = {
                            'id' : eid 
                            , 'instanceId'  : instanceid
                            , 'moduleId'    : tag.module 
                            , 'view'        : viewname
                        };
                        elements.push(elemResource);
                    }
                }

                if (!tagRemoved) {
                    outputBuffer = outputBuffer.concat(copyStartTag(elem, attrs, prefix, uri, namespaces));
                }
                addId(attrs);
            });

            cb.onEndElementNS(function(elem, prefix, uri) {
                if (!tagRemoved) {
                    outputBuffer.push(&quot;&lt;/&quot;, (prefix ? prefix + &quot;:&quot; : &quot;&quot;), elem, &quot;&gt;&quot;);
                } else { }
                tagRemoved = false;
            });

            cb.onCharacters(copy);
            cb.onCdata(copy);
            //cb.onComment(copy);

            function copy(chars) {
                outputBuffer.push(chars);
            };

            cb.onWarning(function(msg) {
                logger.warn('warning ' + msg);
            });

            cb.onError(function(msg) {
                logger.error('<ERROR>'+JSON.stringify(msg)+&quot;&lt;/ERROR&gt;&quot;);
            });
        }
    }
}

add an 'id' attribute to identify the element when inserting its rendered content

  • param: attributes[] attrs attributes from node-xml sax

  • return: String existing or new element id

function addId(attrs) {
    var eid
        // [TBD] use fixed format so file sizes is guaranteed on equal input
        , elemId = &quot;module&quot; + new Date().getTime() + &quot;&quot; + parseInt(Math.random()*10);
    for (var j = 0, al = attrs.length, hasId = false; j &lt; al; j++) {
        if (attrs[j][0] === &quot;id&quot;) {
            eid = attrs[j][1];
            hasId = true;
        }
    }
    if (!hasId) {
        attrs.push([&quot;id&quot;, elemId]);
        return elemId
    } else {
        return eid;
    }
}

function copyStartTag(elem, attrs, prefix, uri, namespaces) {
    var outputBuffer = [];
    outputBuffer.push(&quot;&lt;&quot;, (prefix ? prefix + &quot;:&quot; : &quot;&quot;), elem);
    if (namespaces.length &gt; 0 || attrs.length &gt; 0) {
        outputBuffer.push(&quot; &quot;);
    }
    //for (i = 0; i < namespaces.length; i++) {
    //    outputBuffer.push("xmlns:" + namespaces[i][0] + "=\"" + namespaces[i][1] + "\" ");
    //    savedNamespaces[namespaces[i][1]] = namespaces[i][0]; 
    //}
    for (var i = 0; i &lt; attrs.length; i++) {
        outputBuffer.push(attrs[i][0], '="', attrs[i][1], '"');
        if (i &lt; attrs.length - 1) { 
            outputBuffer.push(' '); 
        }
    }
    outputBuffer.push(['meta', 'link', 'br', 'img', 'input'].indexOf(elem) &gt; -1 ? '/>' : '>');
    return outputBuffer;
}

resourcemanager

../lib/resourcemanager.js

Resources are normally not constructed manually but accessed using this module. If caching is enabled, the Resource Manager takes care of caching.

Todos

  • Resource Resolution
  • Define schemes and transitions:
  • External HTTP URLs --> Routed URLS --> Module-internal URLs --> File system URLs; vice versa
  • Module-specific URIs, e.g. module://weather;1.0.1 maps to [determined by global web component broker]
  • Need clear borders between sub-components for all URL schemes
var resources           = require('./resources.js')
    , mod_events        = require('events')
    , mod_resources     = require('./resources.js')
    , mod_promise       = require('promised-io')
    , mod_path          = require('path')
    , mod_sys           = require('sys')
    , logger            = require('./logger.js').getLogger('ResourceManager')
    , config            = null
    , cache             = null
    , c                 = console.log

// we are in ./lib, document root is always one up
const documentRoot      = mod_path.join(__dirname, '..');

function configure(c, ch) {
    config = c;
    cache = ch;
}

Convenience array style version of getResource

  • param: String[] urls URL to resource

  • return: Promise Promise, is resolved once all resources are loaded

  • publi: c

function getResources(urls) {
    var defer = mod_promise.defer();
    var self = this;
    var u = 
    mod_promise.all(urls.map(function (item) {
                        return self.getResource(item);
                    }))
                    .then(function (all) {
                        defer.resolve(all);
                    });
    return defer.promise;
}

Loads a resource from either a file://, http:// or relative path.

  • param: String url file:// or http:// URL

  • return: Promise Promise, is resolved when resource is ready

  • publi: c

function getResource (url) {
    return loadUrl(url);
    // var defer = mod_promise.defer();
    // loadUrl(url).then(function (res) {
    //     defer.resolve(res);
    // });
    // return defer.promise;
}

Gets a resource either from the cache (if enabled) or loads it.

  • param: String url resource url

  • param: Promise Promise, resolves to resource

  • publi: c

function loadUrl(url) {
    var defer = mod_promise.defer()
        , type = null
        , resource = null

    if (cache) {
        cache.fromCache(url).then(function (resource) {
        if (!resource) {
            resource = getResourceByUrl(url)
                    .then(function (resource) {
                        cache.toCache(url, resource.data);
                        defer.resolve(resource);
                    });
        } else {
            defer.resolve(resource);
        }
        });        
    } else {
        resource = getResourceByUrl(url)
                    .then(function (resource) {
                        defer.resolve(resource);
                    });
    }

    
    return defer.promise;
}

Gets the correct resource type assiocated to a URL without querying the cache.

  • param: String url resource URL

  • param: Promise promise, resolves to resource instance.

  • publi: c

function getResourceByUrl(url) {
    var defer       = mod_promise.defer()
        , resource  = null
        , intUrl    = url
    
    if (url.indexOf('http://') === 0) {                         
        resource = new mod_resources.HttpResource();
    } else if (url.indexOf('file://') === 0) {
        resource = new mod_resources.FileResource();
    } else {                                                    // assume it's a file location
        intUrl = translatePathToFileUrl(url);
        resource = new mod_resources.FileResource();
    }
    //c('getResourceByUrl ' + url + (url != intUrl ? url : ''))
    resource.load(intUrl);
    resource.addListener('stateChanged', function () {
        defer.resolve(resource);         
    });  
        
    return defer.promise;
}

Map a standard file path to a absolute file:// URL. Paths are always assumed relative to an installation root folder.

  • param: String path file path

  • param: String an absolute file:// URL

  • publi: c

function translatePathToFileUrl(path) {
    var p = 'file://'+ mod_path.join(documentRoot, path);
    return p;
}

exports.getResources            = getResources;
exports.getResource             = getResource;
exports.configure               = configure;
exports.translatePathToFileUrl  = translatePathToFileUrl;

resources

../lib/resources.js

Everything is a resource.

Todos

  • Resources should be sealed and/or frozen after loading (state 'complete')

  • type: String

  • api: public

var logger          = require('./logger.js').getLogger('Resource')
    , mod_util      = require('util')
    , mod_events    = require('events')
    , mod_http      = require('http')
    , mod_fs        = require('fs')
    , mod_url       = require('url')

function uuid() {
    return new Date().getTime() + '-' + Math.round(Math.random()*10000);
}
  • class: Base resource class ##

  • param: String URL of resource

  • param: String resource payload

function Resource(url, data) {
    this._state = Resource.STATE.INIT;
    if (typeof url !== 'undefined' &amp;&amp; typeof data !== 'undefined') {
        this.url = url;
        this.data = new Buffer(data);
        this._state = Resource.STATE.READY;
    } else {
        this.data = null;        
    }
    
    this.uuid = uuid();
};
mod_util.inherits(Resource, mod_events.EventEmitter);

Object.defineProperty(Resource.prototype, 'state', {
    get : function () {
        return this._state;
    },
    set : function (val) {
        if ('number' !== typeof val) throw Error('number expected, got ' + val);
        if (val !== this._state) {
            this._state = val;
            this.emit('stateChanged', this);
        }
    }
});

Add a resource as a dependency to this resource.

  • param: Resource res

Resource.prototype.addDependency = function (res) {
    this._requiredResources[res.uuid] = res;
    var self = this;
    res.addListener('stateChanged', function () { 
        //logger.debug('state changed from sub ' + this.uuid);
        self._onDepStateChange.call(self, res);
    });
};

Resource.prototype._onDepStateChange = function (resource) {
    //logger.debug('_onDepStateChange from ' + resource.uuid + '@' + this.uuid);
    var state = Resource.STATE.READY;
    for (rn in this._requiredResources) {
        state = Math.min(this._requiredResources[rn].state, state);
    }
    this.state = state;
};

Must be implemented by subclasses.

Resource.prototype.load = function () {
    throw new Error('not implemented');
}

Resource.STATE = {
    INIT        : 0
    , LOADING     : 2
    , WAITING     : 4
    , READY       : 6
};

exports.Resource = Resource;
  • class: FileResource

  • param: String file URL of resource

  • param: String resource payload

function FileResource() {
    Resource.apply(this, arguments);
}
mod_util.inherits(FileResource, Resource);

  • param: String file file URL of resource

FileResource.prototype.load = function (file) {
    if ('undefined' === typeof file || file.indexOf('file://') !== 0) { 
        throw new Error('missing or faulty argument'); 
    }
    this.url = file;
    this.state = Resource.STATE.LOADING;
    var self = this,
        file = file.substring(7);
    mod_fs.readFile(file, function (err, data) {
        if (err) { throw err; }
        self.data = data;
        self.state = Resource.STATE.READY;
    });
};
  • class: HttpResource

  • param: String http URL of resource

  • param: String resource payload

function HttpResource() {
    Resource.apply(this);
}
mod_util.inherits(HttpResource, Resource);

  • param: String url http URL of resource

HttpResource.prototype.load =  function (url) {
    if ('undefined' === typeof url || url.indexOf('http://') !== 0) { 
        throw new Error('missing or faulty argument'); 
    }
    var self = this,
        req;
    this.url = url;
    this.state = Resource.STATE.LOADING;
    this.content = '';
    this._options = mod_url.parse(url);
    
    req = mod_http.get(this._options, function (res) {
        if (res.statusCode &gt;= 400 &amp;&amp; res.statusCode &lt;= 500) {
            throw new Error('URL could not be loaded, code ' + res.statusCode 
                + ', url ' + self._options.host + ':' + self._options.port + self._options.path);
        }
        res.on('data', function (data) {
            self.data = data;
        });        
        res.on('end', function () {
            self.state = Resource.STATE.READY;
        });
    });
};
  • class: HttpResource

  • param: String url http URL of view template resource

  • param: Buffer data

  • param: String[] cssdeps

  • param: String[] scriptdeps

  • param: String[] localdeps

function RenderedViewResource(url, data, elements, cssdeps, scriptdeps, localedeps) {
    Resource.call(this, url, data);
    this.elements = elements;
    this.cssdeps = cssdeps;
    this.scriptdeps = scriptdeps;
    this.localedeps = localedeps;
}


RenderedViewResource.prototype.toString = function () {
    process.exit();
    return this.url + ',' + this.cssdeps + '' + this.scriptdeps + ',' + this.localedeps;
}
mod_util.inherits(RenderedViewResource, Resource);

Resource.prototype.toString = function () {
    process.exit();
}

exports.FileResource = FileResource;
exports.HttpResource = HttpResource;
exports.RenderedViewResource = RenderedViewResource;

server

../lib/server.js

main server.

var mod_connect         = require('connect')
    , mod_sys               = require('sys')
    , mod_path              = require('path')
    , mod_resourceservice   = null
    , mod_resourcemanager   = require('./resourcemanager.js')
    , mod_tagmanager        = require('./tagmanager.js')
    , mod_cache             = require('./Cache.js')
    , mod_socketio          = require('./socketio.js')   
    , mod_modules           = require('./modules.js')
    , mod_fs                = require('fs')
    , cache                 = null
    , logger                = require('./logger.js').getLogger('Server')

if (process.argv.length &lt; 3) {
    logger.error('usage: ...');
    process.exit();
}

// [TBD] handle arguments properly
// [TBD] dynamic config service 
var config = null;
logger.info('reading config from ' + process.argv[2]); 
mod_fs.readFile(process.argv[2], function (err, data) {
    if (err) {
        logger.error('error reading configuration');
        process.exit();
    }
    config = JSON.parse(data);
    logger.info('config loaded');
    mod_cache.configure(config);
    mod_resourcemanager.configure(config, mod_cache);
    mod_resourceservice = require('./resourceservice.js')(config, mod_resourcemanager);
    mod_tagmanager.setTagList(config.taglib);
    mod_modules.configure(config, mod_tagmanager);
    createServer(config);
});

function createServer(config) {
    var server = mod_connect.createServer(
        mod_connect.favicon()
        , mod_connect.router(function (app) {
                app.get(/^\/modules\/([^\.]*)\/(.*\.js)$/,          mod_modules.handleScriptRequest);
                app.get(/^\/modules\/([^\.]*)\/(.*\.html)$/,        mod_modules.handleViewRequest);            
                app.get(/^\/modules\/([^\.]*)\/controller\/(.*)$/,  mod_modules.handleControllerRequest);
                app.get(/^\/resources(.*)$/,                        mod_resourceservice.handleRequest);
                //app.get(/instances\/(.*)/,                          mod_instances.handleInstanceRequest);
            }
        )
        , mod_connect.static(config.server.documentRoot)        
        , mod_connect.logger()            
    );
    
    //var io = require('socket.io').listen(server);
    //mod_socketio.init(io);
    server.listen(config.server.port);
}

// process.on('SIGINT', function () {
//  process.exit();
// });

resourceservice

../lib/resourceservice.js

The resource service, code name "cyclone", is a center piece of Rain. Clients can call it with a list of resources separated by semicolons, that are loaded and concatenated in the order of their occurence. Rain currently creates request markup to this service in the render process.

Examples

<link rel="stylesheet" type="text/css" href="/resources?files=/modules/scrollabletable/main.css ;/modules/domains/main.css;/modules/weather/main.css;/modules/app/application.css ;/modules/app/jquery-ui/css/smoothness/jquery-ui-1.8.14.custom.css"/>

<script type="application/javascript" src="/resources?files=/modules/app/require-jquery.js ;/modules/app/jquery-ui/js/jquery-ui-1.8.14.custom.min.js ;/modules/app/js/socket.io/socket.io.js"></script>

A file must follow some simple rules to be loadable by the resource service:

  • For URLs, e.g. for background images, always use the url() notation, since the parsers won't recognize them otherwise.

Todos

  • refactor to use the resource manager
  • add error handling for files not found
  • rewrite URLs used url() in CSS files
  • connect to redis for caching
  • support for HTTP files
  • correct distinction between local and remote file by protocol
  • put the little fucker into its own worker

module.exports = function (c, resmgr) {

	var  mod_fs		 		= require('fs')
		, mod_url			= require('url')
		, mod_http			= require('http')
		, mod_promise		= require('promised-io')
		, mod_path			= require('path')
		, mod_cssnormalizer = require('./cssnormalizer.js')
		, logger			= require('./logger.js').getLogger('ResourceService')
		, config 			= c
		, resourceManager   = resmgr

	if (!config || !resmgr) { throw new Error('dependencies missing'); }
 * Handles resource requests, usually created by the render engine when scanning a template file.  
 * Resource requests are aggregated requests to CSS or JavaScript files, both can not be mixed
 * into a single request. The correct mime type is determined by the suffix of the first 
 * file.  
 * 
 * @param {request} req Request object
 * @param {response} res Response object 
 * @param {response} next Next connect middleware routing rule
 * @public
 
function handleRequest (req, res, next) {
		var url = mod_url.parse(req.url, true)
			, files = url.query.files.indexOf(';') === -1 
				? [url.query.files] 
				: url.query.files.split(';')
			, isCss = files[0].lastIndexOf('.css') == files[0].length - 4
			, absFiles;

		// The config must be present for computing the absolute file path of a resource. 
		if (!config || !resourceManager) {
			logger.error('config not found');
			return;
		}

		files = files.map(function (url) {
			return config.server.serverRoot + url.replace(new RegExp(&quot;^(/modules\/[^\/]+)"), "$1/htdocs/&quot;)
		})

		resourceManager.getResources(files).then(function (resources) {
			var output = [];
			resources.forEach(function (resource) {
				output.push('\n\n\n\/\*\* FILE ', resource.url, ' */\n', resource.data.toString());
			});
			res.end(output.join(''));
		});
	}

	return {
		'handleRequest' : handleRequest
	}
};

socketio

../lib/socketio.js

This does not work yet as the connection is lost on re-loading the server. Client reload should go into the 'demon' script (to be build from the ashes of run.js)

function init (io) {
	io.sockets.on('connection', function (socket) {
		console.log('client connected');
		socket.on('rain.application.state', function (from, msg) {
	    	console.log('rain.application.state message', from, ':', msg);
		});

		socket.on('disconnect', function () {
			io.sockets.emit('user disconnected');
		});
	});
}

function send (io) {
	console.log('send reload request');
	io.sockets.emit('rain.application.reload', { 'reload' : true } );
}

exports.init = init;

tagmanager

../lib/tagmanager.js

The Tag Manager handles the mappings between CSS selectors on view templates and web components.

Todos

  • Make it dynamicly configurable
var logger        = require('./logger.js').getLogger('TagManager');

function TagManager() {

  var taglist = [];

  this.addTag = function (tag) {
    taglist.push(tag);
  }

  this.setTagList = function (tags) {
    taglist = tags
  }

[TBD] This code sucks big time... I need to organize tags more efficiently to get rid of this ugly and buggy piece of code. Sizzle could be used, but it's most probably way too slow.

Todos

  • find best match, selectors with predicated must be ranked higher
  • more than one module my map to a single element
this.getTag = function (elem, attrs, prefix, uri, namespaces) {
    //logger.debug(JSON.stringify(arguments));
    for (var i = 0, tag = null; i &lt; taglist.length; i++) {
      tag = taglist[i];
      if (uri !== null &amp;&amp; tag.namespace != '' &amp;&amp; tag.namespace != uri) {
        //logger.debug('tag has namespace, but no match'); 
        continue;
      }

      var si = tag.selector.indexOf('[');
      if (si !== -1) {
        var tagelem = tag.selector.substring(0, si),
            preds = {},
            ps, 
            attrh = {};
        ps = tag.selector.substring(si).split(']').forEach(function (val) {
          if (val === '') return;
          preds[val.substring(1, val.indexOf('='))] = val.substring(val.indexOf('=') + 1);
        });
        for (var j = 0; j &lt; attrs.length; j++) {
          attrh[attrs[j][0]] = attrs[j][1];
        }
      } else {
        var tagelem = tag.selector;
      }

      var fe = true;
      if (tagelem !== elem &amp;&amp; tagelem !== '*') {
        //logger.debug('element name not matched');
        fe = false;
        continue;
      }

      var fp = true
      if (si !== -1) {
        var count = 0;
        for (pred in preds) {
          //logger.debug('pred ' + pred + ". " + attrh[pred] + "," + preds[pred]);
          if (attrh[pred] !== preds[pred]) {
            fp = false;
            //logger.debug('attribute val not mached');
            break;
          }
          count++;
        }
        if (count -1 &gt;= j) {
          fp = false;
          //logger.debug('matched not all predicates');  
          break;
        }
      }

      if (!fp) {
        //logger.debug('attrs not matched');
        continue;
      }
      if (!fe) {
        //logger.debug('selector not matched');
        continue;
      }

      //logger.debug('matching tag ' + tag.selector);
      return tag;
    }

    return null;
  }
}

module.exports = new TagManager();

modulecontainer

../lib/modulecontainer.js

Module Container, Manager, Factory, whatever. All module related thingz must belong to us.

Todos

  • scan the local module folder for web component descriptors as resources.
  • download module descriptors from remote web component hosts as resources.
  • provide web component objects.
  • crud on module instances.

var mod_fs = require('fs')
	, mod_resourcemanager = require('./resourcemanager.js')
	, path = require('path')



function moduleRootFolder() {
	return path.join(__dirname, '..', 'modules');
}

function scanFolder(folder) {
	mod_fs.readdir(moduleRootFolder(), function (err, files) {
		if (err) throw err;

		mod_resourcemanager.getResources(['file://' + moduleRootFolder() + '/app/htdocs/index.html']).then(function () {
			console.log('grunz...');	
		}); 
		
	});
}

scanFolder();