Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 291 lines (253 sloc) 8.01 kB
22f08f0 @robfig Refactor mvc.go into {mvc,controller,session,flash,params}.go
authored
1 package rev
2
3 import (
4 "database/sql"
5 "fmt"
6 "net/http"
7 "os"
8 "path/filepath"
9 "reflect"
10 "runtime"
11 "runtime/debug"
12 "strings"
13 )
14
15 type Controller struct {
16 Name string
17 Type *ControllerType
18 MethodType *MethodType
19
20 Request *Request
21 Response *Response
22
23 Flash Flash // User cookie, cleared after each request.
24 Session Session // Session, stored in cookie, signed.
25 Params *Params // Parameters from URL and form (including multipart).
26 Args map[string]interface{} // Per-request scratch space.
27 RenderArgs map[string]interface{} // Args passed to the template.
28 Validation *Validation // Data validation helpers
29 Txn *sql.Tx // Nil by default, but may be used by the app / plugins
30 }
31
32 func NewController(req *Request, resp *Response, ct *ControllerType) *Controller {
33 return &Controller{
34 Name: ct.Type.Name(),
35 Type: ct,
36 Request: req,
37 Response: resp,
38 Params: ParseParams(req),
39 RenderArgs: map[string]interface{}{
40 "RunMode": RunMode,
41 },
42 }
43 }
44
45 func (c *Controller) FlashParams() {
46 for key, vals := range c.Params.Values {
47 c.Flash.Out[key] = vals[0]
48 }
49 }
50
51 func (c *Controller) SetCookie(cookie *http.Cookie) {
52 http.SetCookie(c.Response.Out, cookie)
53 }
54
55 // Invoke the given method, save headers/cookies to the response, and apply the
56 // result. (e.g. render a template to the response)
57 func (c *Controller) Invoke(appControllerPtr reflect.Value, method reflect.Value, methodArgs []reflect.Value) {
58
59 // Handle panics.
60 defer func() {
61 if err := recover(); err != nil {
62 handleInvocationPanic(c, err)
63 }
64 }()
65
66 // Clean up from the request.
67 defer func() {
68 // Delete temp files.
69 if c.Request.MultipartForm != nil {
70 err := c.Request.MultipartForm.RemoveAll()
71 if err != nil {
72 WARN.Println("Error removing temporary files:", err)
73 }
74 }
75
76 for _, tmpFile := range c.Params.tmpFiles {
77 err := os.Remove(tmpFile.Name())
78 if err != nil {
79 WARN.Println("Could not remove upload temp file:", err)
80 }
81 }
82 }()
83
84 // Run the plugins.
85 plugins.BeforeRequest(c)
86
87 // Calculate the Result by running the interceptors and the action.
88 resultValue := func() reflect.Value {
89 // Call the BEFORE interceptors
90 result := c.invokeInterceptors(BEFORE, appControllerPtr)
91 if result != nil {
92 return reflect.ValueOf(result)
93 }
94
95 // Invoke the action.
96 resultValue := method.Call(methodArgs)[0]
97
98 // Call the AFTER interceptors
99 result = c.invokeInterceptors(AFTER, appControllerPtr)
100 if result != nil {
101 return reflect.ValueOf(result)
102 }
103 return resultValue
104 }()
105
106 plugins.AfterRequest(c)
107
108 if resultValue.IsNil() {
109 return
110 }
111 result := resultValue.Interface().(Result)
112
113 // Apply the result, which generally results in the ResponseWriter getting written.
114 result.Apply(c.Request, c.Response)
115 }
116
117 // This function handles a panic in an action invocation.
118 // It cleans up the stack trace, logs it, and displays an error page.
119 func handleInvocationPanic(c *Controller, err interface{}) {
120 plugins.OnException(c, err)
121 stack := string(debug.Stack())
122 ERROR.Println(err, "\n", stack)
123
124 error := NewErrorFromPanic(err)
125 if error == nil {
126 c.Response.Out.WriteHeader(500)
127 c.Response.Out.Write([]byte(stack))
128 return
129 }
130
131 c.RenderError(error).Apply(c.Request, c.Response)
132 }
133
134 func (c *Controller) invokeInterceptors(when InterceptTime, appControllerPtr reflect.Value) Result {
135 var result Result
136 for _, intc := range getInterceptors(when, appControllerPtr) {
137 resultValue := intc.Invoke(appControllerPtr)
138 if !resultValue.IsNil() {
139 result = resultValue.Interface().(Result)
140 }
141 if when == BEFORE && result != nil {
142 return result
143 }
144 }
145 return result
146 }
147
148 func (c *Controller) RenderError(err error) Result {
149 return ErrorResult{c.RenderArgs, err}
150 }
151
152 // Render a template corresponding to the calling Controller method.
153 // Arguments will be added to c.RenderArgs prior to rendering the template.
154 // They are keyed on their local identifier.
155 //
156 // For example:
157 //
158 // func (c Users) ShowUser(id int) rev.Result {
159 // user := loadUser(id)
160 // return c.Render(user)
161 // }
162 //
163 // This action will render views/Users/ShowUser.html, passing in an extra
164 // key-value "user": (User).
165 func (c *Controller) Render(extraRenderArgs ...interface{}) Result {
166 // Get the calling function name.
167 pc, _, line, ok := runtime.Caller(1)
168 if !ok {
169 ERROR.Println("Failed to get Caller information")
170 return nil
171 }
172 // e.g. sample/app/controllers.(*Application).Index
173 var fqViewName string = runtime.FuncForPC(pc).Name()
174 var viewName string = fqViewName[strings.LastIndex(fqViewName, ".")+1 : len(fqViewName)]
175
176 // Determine what method we are in.
177 // (e.g. the invoked controller method might have delegated to another method)
178 methodType := c.MethodType
179 if methodType.Name != viewName {
180 methodType = c.Type.Method(viewName)
181 if methodType == nil {
182 return c.RenderError(fmt.Errorf(
183 "No Method %s in Controller %s when loading the view."+
184 " (delegating Render is only supported within the same controller)",
185 viewName, c.Name))
186 }
187 }
188
189 // Get the extra RenderArgs passed in.
190 if renderArgNames, ok := methodType.RenderArgNames[line]; ok {
191 if len(renderArgNames) == len(extraRenderArgs) {
192 for i, extraRenderArg := range extraRenderArgs {
193 c.RenderArgs[renderArgNames[i]] = extraRenderArg
194 }
195 } else {
196 ERROR.Println(len(renderArgNames), "RenderArg names found for",
197 len(extraRenderArgs), "extra RenderArgs")
198 }
199 } else {
200 ERROR.Println("No RenderArg names found for Render call on line", line,
201 "(Method", methodType, ", ViewName", viewName, ")")
202 }
203
204 return c.RenderTemplate(c.Name + "/" + viewName + ".html")
205 }
206
207 // A less magical way to render a template.
208 // Renders the given template, using the current RenderArgs.
209 func (c *Controller) RenderTemplate(templatePath string) Result {
210
211 // Get the Template.
212 template, err := MainTemplateLoader.Template(templatePath)
213 if err != nil {
214 return c.RenderError(err)
215 }
216
217 return &RenderTemplateResult{
218 Template: template,
219 RenderArgs: c.RenderArgs,
220 }
221 }
222
223 // Uses encoding/json.Marshal to return JSON to the client.
224 func (c *Controller) RenderJson(o interface{}) Result {
225 return RenderJsonResult{o}
226 }
227
228 // Uses encoding/xml.Marshal to return XML to the client.
229 func (c *Controller) RenderXml(o interface{}) Result {
230 return RenderXmlResult{o}
231 }
232
233 // Render plaintext in response, printf style.
234 func (c *Controller) RenderText(text string, objs ...interface{}) Result {
235 finalText := text
236 if len(objs) > 0 {
237 finalText = fmt.Sprintf(text, objs)
238 }
239 return &RenderTextResult{finalText}
240 }
241
242 // Render a "todo" indicating that the action isn't done yet.
243 func (c *Controller) Todo() Result {
244 c.Response.Status = http.StatusNotImplemented
245 return c.RenderError(&Error{
246 Title: "TODO",
247 Description: "This action is not implemented",
248 })
249 }
250
251 func (c *Controller) NotFound(msg string) Result {
252 c.Response.Status = http.StatusNotFound
253 return c.RenderError(&Error{
254 Title: "Not Found",
255 Description: msg,
256 })
257 }
258
259 // Return a file, either displayed inline or downloaded as an attachment.
260 // The name and size are taken from the file info.
261 func (c *Controller) RenderFile(file *os.File, delivery ContentDisposition) Result {
262 var length int64 = -1
263 fileInfo, err := file.Stat()
264 if err != nil {
265 WARN.Println("RenderFile error:", err)
266 }
267 if fileInfo != nil {
268 length = fileInfo.Size()
269 }
270 return &BinaryResult{
271 Reader: file,
272 Name: filepath.Base(file.Name()),
273 Length: length,
274 Delivery: delivery,
275 }
276 }
277
278 // Redirect to an action or to a URL.
279 // c.Redirect(Controller.Action)
280 // c.Redirect("/controller/action")
281 // c.Redirect("/controller/%d/action", id)
282 func (c *Controller) Redirect(val interface{}, args ...interface{}) Result {
283 if url, ok := val.(string); ok {
284 if len(args) == 0 {
285 return &RedirectToUrlResult{url}
286 }
287 return &RedirectToUrlResult{fmt.Sprintf(url, args...)}
288 }
289 return &RedirectToActionResult{val}
290 }
Something went wrong with that request. Please try again.