1 | /**
|
2 | * Copyright 2014 Skytap Inc.
|
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 _ = require('underscore'),
|
18 | extend = require('extend'),
|
19 | Promise = require('bluebird'),
|
20 | events = require('events'),
|
21 | Logger = require('./logger'),
|
22 | Config = require('./config'),
|
23 | Template = require('./template'),
|
24 | Environment = require('./environment'),
|
25 | Indentifier = require('./identifier'),
|
26 | RenderError = require('./errors/rendererror');
|
27 |
|
28 | /**
|
29 | * Module with common controller functionality.
|
30 | **/
|
31 | function Controller () {}
|
32 |
|
33 | _.extend(Controller.prototype, events.EventEmitter.prototype, {
|
34 |
|
35 | DEFAULT_ERROR_MESSAGE : 'An error occurred. Please try again.',
|
36 |
|
37 | ERROR_TEMPLATE: 'error',
|
38 |
|
39 | before : {},
|
40 |
|
41 | templatePath : '/lib/template',
|
42 |
|
43 | filtersByRoute : [],
|
44 |
|
45 | //////////////////////////////////////////////////////////////////////////
|
46 | // Boilerplate REST methods /////////////////////////////////////////////
|
47 | ////////////////////////////////////////////////////////////////////////
|
48 |
|
49 | /**
|
50 | * Controller.index(request, response, next)
|
51 | * - request (Object): Express request object
|
52 | * - response (Object): Express response object
|
53 | * - next (Function): Express function to invoke the next handler
|
54 | *
|
55 | * GET /resource
|
56 | **/
|
57 | index : function(request, response, next) {
|
58 | next();
|
59 | },
|
60 |
|
61 | /**
|
62 | * Controller.new(request, response, next)
|
63 | * - request (Object): Express request object
|
64 | * - response (Object): Express response object
|
65 | * - next (Function): Express function to invoke the next handler
|
66 | *
|
67 | * GET /resource/new
|
68 | **/
|
69 | new : function(request, response, next) {
|
70 | next();
|
71 | },
|
72 |
|
73 | /**
|
74 | * Controller.create(request, response, next)
|
75 | * - request (Object): Express request object
|
76 | * - response (Object): Express response object
|
77 | * - next (Function): Express function to invoke the next handler
|
78 | *
|
79 | * POST /resource
|
80 | **/
|
81 | create : function(request, response, next) {
|
82 | next();
|
83 | },
|
84 |
|
85 | /**
|
86 | * Controller.show(request, response, next)
|
87 | * - request (Object): Express request object
|
88 | * - response (Object): Express response object
|
89 | * - next (Function): Express function to invoke the next handler
|
90 | *
|
91 | * GET /resource/:id
|
92 | **/
|
93 | show : function(request, response, next) {
|
94 | next();
|
95 | },
|
96 |
|
97 | /**
|
98 | * Controller.edit(request, response, next)
|
99 | * - request (Object): Express request object
|
100 | * - response (Object): Express response object
|
101 | * - next (Function): Express function to invoke the next handler
|
102 | *
|
103 | * GET /resource/:id/edit
|
104 | **/
|
105 | edit : function(request, response, next) {
|
106 | next();
|
107 | },
|
108 |
|
109 | /**
|
110 | * Controller.update(request, response, next)
|
111 | * - request (Object): Express request object
|
112 | * - response (Object): Express response object
|
113 | * - next (Function): Express function to invoke the next handler
|
114 | *
|
115 | * PUT /resource/:id
|
116 | **/
|
117 | update : function(request, response, next) {
|
118 | next();
|
119 | },
|
120 |
|
121 | /**
|
122 | * Controller.destroy(request, response, next)
|
123 | * - request (Object): Express request object
|
124 | * - response (Object): Express response object
|
125 | * - next (Function): Express function to invoke the next handler
|
126 | *
|
127 | * DELETE /resource/:id
|
128 | **/
|
129 | destroy : function(request, response, next) {
|
130 | next();
|
131 | },
|
132 |
|
133 | //////////////////////////////////////////////////////////////////////////
|
134 | // Public methods ///////////////////////////////////////////////////////
|
135 | ////////////////////////////////////////////////////////////////////////
|
136 |
|
137 | /**
|
138 | * Controller.addFiltersForHandler(url, handler)
|
139 | * - url (String)
|
140 | * - handler (String)
|
141 | **/
|
142 | addFiltersForHandler : function (url, handler) {
|
143 | var self = this;
|
144 |
|
145 | if (this.filtersByRoute[url] === undefined) {
|
146 | this.filtersByRoute[url] = {};
|
147 | }
|
148 |
|
149 | this.filtersByRoute[url][handler] = [];
|
150 |
|
151 | _.each(this.before, function cacheFilters (handlers, filter) {
|
152 | if (handlers.indexOf(handler) !== -1 || handlers[0] === 'all') {
|
153 | self.filtersByRoute[url][handler].push(filter);
|
154 | }
|
155 | });
|
156 | },
|
157 |
|
158 | /**
|
159 | * Controller.getFilters(url, handler) -> Array
|
160 | * - url (String)
|
161 | * - handler (String)
|
162 | **/
|
163 | getFilters : function(url, handler) {
|
164 | return this.filtersByRoute[url][handler];
|
165 | },
|
166 |
|
167 | /**
|
168 | * Controller.render(request, response, templateName, templateValues, send) -> Promise
|
169 | * - request (Object): Express request object
|
170 | * - response (Object): Express response object
|
171 | * - templateName (String)
|
172 | * - templateValues (Object)
|
173 | * - send (Boolean)
|
174 | **/
|
175 | render : function (request, response, templateName, templateValues, send) {
|
176 | var self = this,
|
177 | defaults = {
|
178 | layout : 'layouts/application',
|
179 | title : '',
|
180 | stylesheets : [],
|
181 | response : response,
|
182 | request : request,
|
183 | flash : {},
|
184 | Config : Config,
|
185 | _content : {}
|
186 | },
|
187 | templateMixins = Template.getMixins(),
|
188 | send = (typeof send === 'undefined') ? true : send,
|
189 | startTime;
|
190 |
|
191 | return new Promise(function (resolve, reject) {
|
192 | templateValues = _.extend(
|
193 | defaults,
|
194 | templateValues
|
195 | );
|
196 |
|
197 | _.each(templateMixins, function (Helper, name) {
|
198 | templateValues[name] = Helper.bind(templateValues);
|
199 | });
|
200 |
|
201 | _.each(templateValues, function (value, key) {
|
202 | response.locals[key] = value;
|
203 | });
|
204 |
|
205 | startTime = Date.now();
|
206 |
|
207 | response.on('finish', function () {
|
208 | self.emit('render-finished', self, request, startTime, templateName);
|
209 | self._requestFinished(request);
|
210 | });
|
211 |
|
212 | response.render(templateName, { layout : templateValues.layout }, function (error, result) {
|
213 | Logger.debug(request.method + ' ' + request.url + ' DONE', request);
|
214 |
|
215 | Logger.info(
|
216 | 'Request for ' + request.minorjs.controller.name + '#' +
|
217 | request.minorjs.controller.action + ' done in ' +
|
218 | (Date.now() - request.minorjs.start) + 'ms',
|
219 | request
|
220 | );
|
221 |
|
222 | if (error) {
|
223 | self._handleRenderError(request, response, error, templateName);
|
224 | reject(new RenderError());
|
225 | } else {
|
226 | if (send) {
|
227 | response.send(result);
|
228 | }
|
229 | resolve(result);
|
230 | }
|
231 | });
|
232 | });
|
233 | },
|
234 |
|
235 | //////////////////////////////////////////////////////////////////////////
|
236 | // Psuedo-private methods ///////////////////////////////////////////////
|
237 | ////////////////////////////////////////////////////////////////////////
|
238 |
|
239 | /**
|
240 | * Controller._getFullError() -> String
|
241 | **/
|
242 | _getFullError : function (request, response, error, incidentId) {
|
243 | return 'Incident ' + incidentId + ' - ' + error.stack;
|
244 | },
|
245 |
|
246 | /**
|
247 | * Controller._getUserError() -> String
|
248 | **/
|
249 | _getUserError : function (request, response, error, incidentId) {
|
250 | return this.DEFAULT_ERROR_MESSAGE + ' (' + incidentId + ')';
|
251 | },
|
252 |
|
253 | /**
|
254 | * Controller._handleError(request, response, error)
|
255 | **/
|
256 | _handleError : function (request, response, error) {
|
257 | if (error instanceof RenderError) {
|
258 | // error was already handled
|
259 | return;
|
260 | }
|
261 |
|
262 | var incidentId = Indentifier.generate(),
|
263 | fullError = this._getFullError(request, response, error, incidentId),
|
264 | userError;
|
265 |
|
266 | Logger.error(fullError, request);
|
267 |
|
268 | if (request.xhr) {
|
269 | this._handleXhrError(request, response, error);
|
270 | } else {
|
271 | this._handleNormalError(request, response, error, incidentId);
|
272 | }
|
273 | },
|
274 |
|
275 | /**
|
276 | * Controller._handleXhrError(request, response, error)
|
277 | **/
|
278 | _handleXhrError : function (request, response, error) {
|
279 | response.status(500).send(
|
280 | {
|
281 | success : false,
|
282 | error : error
|
283 | }
|
284 | );
|
285 | },
|
286 |
|
287 | /**
|
288 | * Controller._handleNormalError(request, response, error, incidentId)
|
289 | **/
|
290 | _handleNormalError : function (request, response, error, incidentId) {
|
291 | var userError = this._getUserError(request, response, error, incidentId);
|
292 |
|
293 | response.status(500);
|
294 |
|
295 | this.render(
|
296 | request,
|
297 | response,
|
298 | this.ERROR_TEMPLATE,
|
299 | {
|
300 | userError : userError,
|
301 | error : Environment.isDevelopment() ? error : null,
|
302 | layout : 'layouts/error'
|
303 | }
|
304 | );
|
305 | },
|
306 |
|
307 | /**
|
308 | * Controller._handleRenderError(request, response, error, templateName)
|
309 | * - request (Object): Request object.
|
310 | * - response (Object): Response object.
|
311 | * - error (Object)
|
312 | * - templateName (String)
|
313 | **/
|
314 | _handleRenderError: function (request, response, error, templateName) {
|
315 | if (templateName === this.ERROR_TEMPLATE) {
|
316 | var incidentId = Indentifier.generate(),
|
317 | fullError = this._getFullError(request, response, error, incidentId),
|
318 | userError = this._getUserError(request, response, error, incidentId);
|
319 | Logger.error('Could not render error page. ' + fullError, request);
|
320 | response.send(userError);
|
321 | } else {
|
322 | this._handleError(request, response, error);
|
323 | }
|
324 | },
|
325 |
|
326 | /**
|
327 | * Controller._requestFinished(request)
|
328 | * - request (Object): Request object.
|
329 | **/
|
330 | _requestFinished : function (request) {
|
331 | var results = extend(
|
332 | {
|
333 | method : request.method,
|
334 | time : Date.now() - request.minorjs.start
|
335 | },
|
336 | request.minorjs
|
337 | );
|
338 | this.emit('request-finished', request, results);
|
339 | }
|
340 | });
|
341 |
|
342 | module.exports = Controller;
|