1 /* 2 * Geddy JavaScript Web development framework 3 * Copyright 2112 Matthew Eernisse (mde@fleegix.org) 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 var geddy = {} 20 , fs = require('fs') 21 , url = require('url') 22 , querystring = require('querystring') 23 , path = require('path') 24 , errors = require('./errors') 25 , response = require('./response') 26 , model = require('./model') 27 , utils = require('./utils/index') 28 , inflection = require('../deps/inflection') 29 , Worker = require('./worker').Worker 30 , FunctionRouter = require('./routers/function_router').FunctionRouter 31 , RegExpRouter = require('./routers/regexp_router').RegExpRouter 32 , BaseController = require('./base_controller').BaseController 33 , sessions = require('./sessions') 34 , CookieCollection = require('./cookies').CookieCollection 35 , dir = process.cwd() 36 , worker = new Worker() 37 , vm = require('vm') 38 , exec = require('child_process').exec; 39 40 geddy.mixin = utils.mixin 41 geddy.objectToString = utils.objectToString; // WTF 42 geddy.string = utils.string; 43 geddy.async = utils.async; 44 geddy.uri = utils.uri; 45 geddy.inflection = inflection; 46 geddy.model = model; 47 48 geddy.mixin(geddy, new (function () { 49 50 // Load controller ctors 51 // ================== 52 var _getControllerConstructors = function (next) { 53 var dirname = '/app/controllers' 54 , dirList = fs.readdirSync(dir + dirname) 55 , fileName 56 , filePath 57 , ctorName 58 59 , ctors = {} 60 , ctor 61 , jsPat = /\.js$/; 62 63 // Dynamically create controller constructors from files in constructors/ 64 for (var i = 0; i < dirList.length; i++) { 65 fileName = dirList[i]; 66 // Any files ending in '.js' -- e.g., 'neil_pearts.js' 67 if (jsPat.test(fileName)) { 68 // Strip the '.js', e.g., 'neil_pearts' 69 fileName = fileName.replace(jsPat, ''); 70 // Convert underscores to camelCase with initial cap, e.g., 'NeilPearts' 71 ctorName = geddy.string.camelize(fileName, true); 72 filePath = dir + dirname + '/' + fileName; 73 // Registers as a constructor, e.g., ctors.NeilPearts = 74 // require('/path/to/geddy_app/<dirname>/neil_pearts').NeilPearts 75 ctors[ctorName] = require(filePath)[ctorName]; 76 } 77 } 78 for (var p in ctors) { 79 ctor = ctors[p]; 80 ctor.origPrototype = ctor.prototype; 81 } 82 this.controllerRegistry = ctors; 83 next(); 84 } 85 86 // Load the router 87 // ================== 88 , _loadRouter = function (next) { 89 router = require(dir + '/config/router'); 90 router = router.router || router; 91 this.router = router; 92 next(); 93 } 94 95 // Connect to session-store 96 // ================== 97 , _loadSessionStore = function (next) { 98 sessionsConfig = this.config.sessions; 99 if (sessionsConfig) { 100 sessions.createStore(sessionsConfig.store, next); 101 } 102 else { 103 next(); 104 } 105 } 106 107 // Register template-paths 108 // ================== 109 , _registerTemplatePaths = function (next) { 110 var self = this 111 , viewsPath = dir + '/app/views'; 112 // May be running entirely viewless 113 if (!path.existsSync(viewsPath)) { 114 self.templateRegistry = {}; 115 next(); 116 } 117 else { 118 exec('find ' + viewsPath, function (err, stdout, stderr) { 119 var templates 120 , files 121 , file 122 , pat = /\.ejs$/; 123 if (err) { 124 throw err; 125 } 126 else if (stderr) { 127 console.log('Error: ' + stderr); 128 } 129 else { 130 templates = {}; 131 files = stdout.split('\n'); 132 for (var i = 0; i < files.length; i++) { 133 file = files[i]; 134 if (pat.test(file)) { 135 file = file.replace(dir + '/', ''); 136 templates[file] = true; 137 } 138 } 139 self.templateRegistry = templates; 140 next(); 141 } 142 }); 143 } 144 }; 145 146 this.config = null; 147 this.server = null; 148 this.worker = null; 149 this.router = null; 150 this.FunctionRouter = FunctionRouter; 151 this.RegExpRouter = RegExpRouter; 152 this.controllerRegistry = {}; 153 this.templateRegistry = {}; 154 155 this.init = function () { 156 var self = this 157 , items 158 , chain; 159 160 // Set up some aliases 161 this.worker = worker; 162 this.server = worker.server; 163 this.config = worker.config; 164 165 items = [ 166 _getControllerConstructors 167 , _loadRouter 168 , _loadSessionStore 169 , _registerTemplatePaths 170 ]; 171 172 chain = new geddy.async.SimpleAsyncChain(items, this); 173 chain.last = function () { 174 175 // ############### 176 // FIXME: App should not reference the worker 177 worker.config = self.config; 178 // ############### 179 180 self.start(); 181 }; 182 183 chain.run(); 184 }; 185 186 this.start = function () { 187 var self = this 188 , ctors = this.controllerRegistry 189 , router = this.router; 190 191 // Handle the requests 192 // ================== 193 this.server.addListener('request', function (req, resp) { 194 var params 195 , urlParams 196 , ctor 197 , appCtor 198 , baseController 199 , controller 200 , staticPath 201 , staticResp 202 , err 203 , errResp 204 , body = '' 205 , bodyParams 206 , steps = { 207 parseBody: false 208 , sessions: false 209 } 210 , finish; 211 212 finish = function (step) { 213 steps[step] = true; 214 for (var p in steps) { 215 if (!steps[p]) { 216 return false; 217 } 218 } 219 220 controller._handleAction.call(controller, params.action); 221 }; 222 223 //TODO: get better logs (including http status codes) 224 // by wrapping serverResponse.end() 225 req.addListener('end', function () { 226 self.log.access(req.connection.remoteAddress + 227 " " + new Date() + " " + req.method + " " + req.url); 228 }); 229 230 self.requestTime = (new Date()).getTime(); 231 232 if (router) { 233 params = router.first(req); 234 } 235 if (params) { 236 ctor = ctors[params.controller]; 237 if (ctor) { 238 239 // Parses form input, and merges it with params from 240 // the URL and the query-string to produce a Grand Unified Params object 241 urlParams = url.parse(req.url, true).query 242 geddy.mixin(params, urlParams); 243 // If it's a plain form-post, save the request-body, and parse it into 244 // params as well 245 if ((req.method == 'POST' || req.method == 'PUT') && 246 (req.headers['content-type'].indexOf('form-urlencoded') > -1 || 247 req.headers['content-type'].indexOf('application/json') > -1)) { 248 req.addListener('data', function (data) { 249 body += data.toString(); 250 }); 251 // Handle the request once it's finished 252 req.addListener('end', function () { 253 bodyParams = querystring.parse(body); 254 geddy.mixin(params, bodyParams); 255 req.body = body; 256 finish('parseBody'); 257 }); 258 } 259 else { 260 finish('parseBody'); 261 } 262 263 if (ctors.Application) { 264 appCtor = ctors.Application; 265 appCtor.prototype = utils.enhance(new BaseController(), 266 appCtor.origPrototype); 267 baseController = new appCtor(); 268 } 269 else { 270 baseController = new BaseController(); 271 } 272 ctor.prototype = utils.enhance(baseController, ctor.origPrototype); 273 controller = new ctor(); 274 controller.request = req; 275 controller.response = resp; 276 controller.params = params; 277 controller.name = params.controller; 278 279 controller.cookies = new CookieCollection(req); 280 281 if (self.config.sessions) { 282 controller.session = new sessions.Session(controller, function () { 283 finish('sessions'); 284 }); 285 } 286 else { 287 finish('sessions'); 288 } 289 } 290 // 500 error 291 else { 292 err = new errors.InternalServerError('Controller ' + params.controller + 293 ' not found.'); 294 errResp = new response.Response(resp); 295 errResp.send(err.message, err.statusCode, {'Content-Type': 'text/html'}); 296 } 297 } 298 // Either static or 404 299 else { 300 staticPath = self.config.staticFilePath + '/' + req.url; 301 if (path.existsSync(staticPath)) { 302 staticResp = new response.Response(resp); 303 staticResp.sendFile(staticPath); 304 } 305 else { 306 err = new errors.NotFoundError(req.url + ' not found.'); 307 errResp = new response.Response(resp); 308 errResp.send(err.message, err.statusCode, {'Content-Type': 'text/html'}); 309 } 310 } 311 }); 312 313 }; 314 this.log = worker.log; 315 })()); 316 317 global.geddy = geddy; 318 319 worker.start(function () { 320 geddy.init(); 321 }); 322