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