PageRenderTime 61ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/geddy-core/lib/controller.js

https://github.com/davidcoallier/geddy
JavaScript | 361 lines | 243 code | 36 blank | 82 comment | 44 complexity | 01db782c9502c2a4aeb97ef744726718 MD5 | raw file
  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. var sys = require('sys');
  19. var fs = require('fs');
  20. var errors = require('geddy-core/lib/errors');
  21. var response = require('geddy-core/lib/response');
  22. var Templater = require('geddy-template/lib/adapters/ejs').Templater
  23. var Controller = function (obj) {
  24. var undefined; // Local copy of undefined value
  25. // The http.ServerRequest passed to the 'request' event
  26. // callback function
  27. this.request = null;
  28. // The http.ServerResponse passed to the 'request' event
  29. // callback function
  30. this.response = null;
  31. // The action gets passed these as an argument, but we keep
  32. // them here too to have access to the format for
  33. // content-negotiation
  34. this.params = null;
  35. // Cookies collection, written out in the finish and redirect methods
  36. this.cookies = null;
  37. // The name of the controller constructor function,
  38. // in CamelCase with uppercase initial letter -- use geddy.inflections
  39. // to get the other casing versions
  40. this.name = null;
  41. // Content-type the controller can respond with -- assume
  42. // minimum of plaintext
  43. this.respondsWith = ['txt'];
  44. // Content to respond with -- can be an Object or String
  45. this.content = '';
  46. // High-level set of options which can represent multiple
  47. // content-types
  48. // 'txt', 'json', 'xml', 'html'
  49. this.format = '';
  50. // Content-type of the response -- driven by the format, and
  51. // by what content-types the client accepts
  52. this.contentType = '';
  53. // The template root to look in for partials when rendering templates
  54. // Gets created programmatically based on controller name -- see renderTemplate
  55. this.templateRoot = undefined;
  56. // This will be used for 'before' actions for plugins
  57. this.beforeFilters = [];
  58. // This will be used for 'after' actions for plugins
  59. this.afterFilters = [];
  60. // Override defaults with passed-in options
  61. geddy.util.meta.mixin(this, obj);
  62. };
  63. Controller.prototype = new function () {
  64. this.before = function (filter, o) {
  65. this.addFilter('before', filter, o || {});
  66. };
  67. this.after = function (filter, o) {
  68. this.addFilter('after', filter, o || {});
  69. };
  70. this.addFilter = function (phase, filter, opts) {
  71. var obj = {name: filter};
  72. obj.except = opts.except;
  73. obj.only = opts.only;
  74. this[phase + 'Filters'].push(obj);
  75. }
  76. /**
  77. * Primary entry point for calling the action on a controller
  78. */
  79. this.handleAction = function (action, params) {
  80. var _this = this;
  81. // Wrap the actual action-handling in a callback to use as the 'last'
  82. // method in the async chain of before-filters
  83. var callback = function () {
  84. _this[action].call(_this, params);
  85. };
  86. this.execFilters(action, 'before', callback);
  87. };
  88. this.execFilters = function (action, phase, callback) {
  89. var _this = this;
  90. var filters = this[phase + 'Filters'];
  91. var filter;
  92. var name;
  93. var hook;
  94. var list = [];
  95. var applyFilter;
  96. for (var i = 0; i < filters.length; i++) {
  97. filter = filters[i];
  98. applyFilter = true;
  99. if (filter.only && filter.only != action) {
  100. applyFilter = false;
  101. }
  102. if (filter.except && filter.except == action) {
  103. applyFilter = false;
  104. }
  105. if (applyFilter) {
  106. name = filter.name;
  107. hook = geddy.hooks.collection[name];
  108. hook.args = [_this];
  109. list.push(hook);
  110. }
  111. }
  112. var chain = new geddy.util.async.AsyncChain(list);
  113. chain.last = callback;
  114. chain.run();
  115. };
  116. this.formatters = {
  117. // Right now all we have is JSON and plaintext
  118. // Fuck XML until somebody enterprisey wants it
  119. json: function (content) {
  120. var toJson = content.toJson || content.toJSON;
  121. if (typeof toJson == 'function') {
  122. return toJson.call(content);
  123. }
  124. return JSON.stringify(content);
  125. }
  126. , js: function (content, controller) {
  127. var params = controller.params;
  128. if (!params.callback) {
  129. err = new errors.InternalServerError('JSONP callback not defined.');
  130. controller.error(err);
  131. }
  132. return params.callback + '(' + JSON.stringify(content) + ');';
  133. }
  134. , txt: function (content) {
  135. if (typeof content.toString == 'function') {
  136. return content.toString();
  137. }
  138. return JSON.stringify(content);
  139. }
  140. };
  141. this.redirect = function (redir) {
  142. var url;
  143. if (typeof redir == 'string') {
  144. url = redir;
  145. }
  146. else {
  147. var contr = redir.controller || this.name;
  148. var act = redir.action;
  149. var ext = redir.format || this.params.format;
  150. var id = redir.id;
  151. contr = geddy.util.string.decamelize(contr);
  152. url = '/' + contr;
  153. url += act ? '/' + act : '';
  154. url += id ? '/' + id : '';
  155. if (ext) {
  156. url += '.' + ext;
  157. }
  158. }
  159. var r = new response.Response(this.response);
  160. var headers = {'Location': url};
  161. headers['Set-Cookie'] = this.cookies.serialize();
  162. this.session.close(function () {
  163. r.send('', 302, headers);
  164. });
  165. };
  166. this.error = function (err) {
  167. errors.respond(this.response, err);
  168. };
  169. this.transfer = function (act) {
  170. this.params.action = act;
  171. this[act](this.params);
  172. };
  173. this.respond = function (content, format) {
  174. // format and contentType are set at the same time
  175. var negotiated = this.negotiateContent(format);
  176. this.format = negotiated.format;
  177. this.contentType = negotiated.contentType;
  178. if (!this.contentType) {
  179. var err = new errors.NotAcceptableError('Not an acceptable media type.');
  180. this.error(err);
  181. }
  182. this.formatContentAndFinish(content);
  183. };
  184. this.finish = function () {
  185. var r = new response.Response(this.response);
  186. var headers = {'Content-Type': this.contentType};
  187. headers['Set-Cookie'] = this.cookies.serialize();
  188. var content = this.content;
  189. if (this.session) {
  190. this.session.close(function () {
  191. r.send(content, 200, headers);
  192. });
  193. }
  194. else {
  195. r.send(content, 200, headers);
  196. }
  197. };
  198. this.negotiateContent = function (frmt) {
  199. var format
  200. , contentType
  201. , types = []
  202. , match
  203. , params = this.params
  204. , err
  205. , accepts = this.request.headers.accept
  206. , pat
  207. , wildcard = false;
  208. // If the client doesn't provide an Accept header, assume
  209. // it's happy with anything
  210. if (accepts) {
  211. accepts = accepts.split(',');
  212. }
  213. else {
  214. accepts = ['*/*'];
  215. }
  216. if (frmt) {
  217. types = [frmt];
  218. }
  219. else if (params.format) {
  220. var f = params.format;
  221. // See if we can actually respond with this format,
  222. // i.e., that this one is in the list
  223. if (f && ('|' + this.respondsWith.join('|') + '|').indexOf(
  224. '|' + f + '|') > -1) {
  225. types = [f];
  226. }
  227. }
  228. else {
  229. types = this.respondsWith;
  230. }
  231. // Okay, we have some format-types.
  232. if (types.length) {
  233. // Ignore quality factors for now
  234. for (var i = 0, ii = accepts.length; i < ii; i++) {
  235. accepts[i] = accepts[i].split(';')[0];
  236. if (accepts[i] == '*/*') {
  237. wildcard = true;
  238. }
  239. }
  240. // If agent accepts anything, respond with the controller's first choice
  241. if (wildcard) {
  242. var t = types[0];
  243. format = t;
  244. contentType = response.formatsPreferred[t];
  245. if (!contentType) {
  246. this.throwUndefinedFormatError();
  247. }
  248. }
  249. // Otherwise look through the acceptable formats and see if
  250. // Geddy knows about any of them.
  251. else {
  252. for (var i = 0, ii = types.length; i < ii; i++) {
  253. pat = response.formatPatterns[types[i]];
  254. if (pat) {
  255. for (var j = 0, jj = accepts.length; j < jj; j++) {
  256. match = accepts[j].match(pat);
  257. if (match) {
  258. format = types[i];
  259. contentType = match;
  260. break;
  261. }
  262. }
  263. }
  264. // If respondsWith contains some random format that Geddy doesn't know
  265. // TODO Make it easy for devs to add new formats
  266. else {
  267. this.throwUndefinedFormatError();
  268. }
  269. // Don't look at any more formats if there's a match
  270. if (match) {
  271. break;
  272. }
  273. }
  274. }
  275. }
  276. return {format: format, contentType: contentType};
  277. };
  278. this.throwUndefinedFormatError = function () {
  279. err = new errors.InternalServerError('Format not defined in response.formats.');
  280. this.error(err);
  281. };
  282. this.formatContentAndFinish = function (content) {
  283. if (typeof content == 'string') {
  284. this.content = content;
  285. this.finish();
  286. }
  287. else {
  288. if (this.format) {
  289. this.formatContent(this.format, content);
  290. }
  291. else {
  292. err = new errors.InternalServerError('Unknown format');
  293. this.error(err);
  294. }
  295. }
  296. };
  297. this.formatContent = function (format, data) {
  298. if (format == 'html') {
  299. this.renderTemplate(data);
  300. }
  301. else {
  302. var c = this.formatters[format](data, this);
  303. this.formatContentAndFinish(c);
  304. }
  305. };
  306. this.renderTemplate = function (data) {
  307. var _this = this;
  308. // Calculate the templateRoot if not set
  309. this.templateRoot = this.templateRoot ||
  310. 'app/views/' + geddy.inflections[this.name].filename.plural;
  311. var templater = new Templater();
  312. var content = '';
  313. templater.addListener('data', function (d) {
  314. // Buffer for now, but could stream
  315. content += d;
  316. });
  317. templater.addListener('end', function () {
  318. _this.formatContentAndFinish(content);
  319. });
  320. templater.render(data, [this.templateRoot], this.params.action);
  321. };
  322. }();
  323. exports.Controller = Controller;