github.com/woremacx/kocha@v0.7.1-0.20150731103243-a5889322afc9/controller.go (about) 1 package kocha 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "encoding/xml" 7 "fmt" 8 "io" 9 "mime" 10 "net/http" 11 "net/url" 12 "os" 13 "path/filepath" 14 "reflect" 15 "runtime" 16 "strings" 17 "sync" 18 ) 19 20 var contextPool = &sync.Pool{ 21 New: func() interface{} { 22 return &Context{} 23 }, 24 } 25 26 // Controller is the interface that the request controller. 27 type Controller interface { 28 Getter 29 Poster 30 Putter 31 Deleter 32 Header 33 Patcher 34 } 35 36 // Getter interface is an interface representing a handler for HTTP GET request. 37 type Getter interface { 38 GET(c *Context) error 39 } 40 41 // Poster interface is an interface representing a handler for HTTP POST request. 42 type Poster interface { 43 POST(c *Context) error 44 } 45 46 // Putter interface is an interface representing a handler for HTTP PUT request. 47 type Putter interface { 48 PUT(c *Context) error 49 } 50 51 // Deleter interface is an interface representing a handler for HTTP DELETE request. 52 type Deleter interface { 53 DELETE(c *Context) error 54 } 55 56 // Header interface is an interface representing a handler for HTTP HEAD request. 57 type Header interface { 58 HEAD(c *Context) error 59 } 60 61 // Patcher interface is an interface representing a handler for HTTP PATCH request. 62 type Patcher interface { 63 PATCH(c *Context) error 64 } 65 66 type requestHandler func(c *Context) error 67 68 // DefaultController implements Controller interface. 69 // This can be used to save the trouble to implement all of the methods of 70 // Controller interface. 71 type DefaultController struct { 72 } 73 74 // GET implements Getter interface that renders the HTTP 405 Method Not Allowed. 75 func (dc *DefaultController) GET(c *Context) error { 76 return c.RenderError(http.StatusMethodNotAllowed, nil, nil) 77 } 78 79 // POST implements Poster interface that renders the HTTP 405 Method Not Allowed. 80 func (dc *DefaultController) POST(c *Context) error { 81 return c.RenderError(http.StatusMethodNotAllowed, nil, nil) 82 } 83 84 // PUT implements Putter interface that renders the HTTP 405 Method Not Allowed. 85 func (dc *DefaultController) PUT(c *Context) error { 86 return c.RenderError(http.StatusMethodNotAllowed, nil, nil) 87 } 88 89 // DELETE implements Deleter interface that renders the HTTP 405 Method Not Allowed. 90 func (dc *DefaultController) DELETE(c *Context) error { 91 return c.RenderError(http.StatusMethodNotAllowed, nil, nil) 92 } 93 94 // HEAD implements Header interface that renders the HTTP 405 Method Not Allowed. 95 func (dc *DefaultController) HEAD(c *Context) error { 96 return c.RenderError(http.StatusMethodNotAllowed, nil, nil) 97 } 98 99 // PATCH implements Patcher interface that renders the HTTP 405 Method Not Allowed. 100 func (dc *DefaultController) PATCH(c *Context) error { 101 return c.RenderError(http.StatusMethodNotAllowed, nil, nil) 102 } 103 104 type mimeTypeFormats map[string]string 105 106 // MimeTypeFormats is relation between mime type and file extension. 107 var MimeTypeFormats = mimeTypeFormats{ 108 "application/json": "json", 109 "application/xml": "xml", 110 "text/html": "html", 111 "text/plain": "txt", 112 } 113 114 // Get returns the file extension from the mime type. 115 func (m mimeTypeFormats) Get(mimeType string) string { 116 return m[mimeType] 117 } 118 119 // Set set the file extension to the mime type. 120 func (m mimeTypeFormats) Set(mimeType, format string) { 121 m[mimeType] = format 122 } 123 124 // Del delete the mime type and file extension. 125 func (m mimeTypeFormats) Del(mimeType string) { 126 delete(m, mimeType) 127 } 128 129 // Context represents a context of each request. 130 type Context struct { 131 Name string // route name of the controller. 132 Layout string // layout name. 133 Format string // format of response. 134 Data interface{} // data for template. 135 Request *Request // request. 136 Response *Response // response. 137 Params *Params // parameters of form values. 138 Session Session // session. 139 Flash Flash // flash messages. 140 App *Application // an application. 141 142 // Errors represents the map of errors that related to the form values. 143 // A map key is field name, and value is slice of errors. 144 // Errors will be set by Context.Params.Bind(). 145 Errors map[string][]*ParamError 146 } 147 148 func newContext() *Context { 149 c := contextPool.Get().(*Context) 150 c.reset() 151 return c 152 } 153 154 // ErrorWithLine returns error that added the filename and line to err. 155 func ErrorWithLine(err error) error { 156 return errorWithLine(err, 2) 157 } 158 159 func errorWithLine(err error, calldepth int) error { 160 if _, file, line, ok := runtime.Caller(calldepth); ok { 161 return fmt.Errorf("%s:%d: %v", file, line, err) 162 } 163 return err 164 } 165 166 // Render renders a template. 167 // 168 // A data to used will be determined the according to the following rules. 169 // 170 // 1. If data of any map type is given, it will be merged to Context.Data if possible. 171 // 172 // 2. If data of another type is given, it will be set to Context.Data. 173 // 174 // 3. If data is nil, Context.Data as is. 175 // 176 // Render retrieve a template file from controller name and c.Response.ContentType. 177 // e.g. If controller name is "root" and ContentType is "application/xml", Render will 178 // try to retrieve the template file "root.xml". 179 // Also ContentType set to "text/html" if not specified. 180 func (c *Context) Render(data interface{}) error { 181 if err := c.setData(data); err != nil { 182 return c.errorWithLine(err) 183 } 184 c.setContentTypeIfNotExists("text/html") 185 if err := c.setFormatFromContentTypeIfNotExists(); err != nil { 186 return c.errorWithLine(err) 187 } 188 t, err := c.App.Template.Get(c.App.Config.AppName, c.Layout, c.Name, c.Format) 189 if err != nil { 190 return c.errorWithLine(err) 191 } 192 buf := bufPool.Get().(*bytes.Buffer) 193 defer func() { 194 buf.Reset() 195 bufPool.Put(buf) 196 }() 197 if err := t.Execute(buf, c); err != nil { 198 return fmt.Errorf("%s: %v", t.Name(), err) 199 } 200 if err := c.render(buf); err != nil { 201 return c.errorWithLine(err) 202 } 203 return nil 204 } 205 206 // RenderJSON renders the data as JSON. 207 // 208 // RenderJSON is similar to Render but data will be encoded to JSON. 209 // ContentType set to "application/json" if not specified. 210 func (c *Context) RenderJSON(data interface{}) error { 211 if err := c.setData(data); err != nil { 212 return c.errorWithLine(err) 213 } 214 c.setContentTypeIfNotExists("application/json") 215 buf, err := json.Marshal(c.Data) 216 if err != nil { 217 return c.errorWithLine(err) 218 } 219 if err := c.render(bytes.NewReader(buf)); err != nil { 220 return c.errorWithLine(err) 221 } 222 return nil 223 } 224 225 // RenderXML renders the data as XML. 226 // 227 // RenderXML is similar to Render but data will be encoded to XML. 228 // ContentType set to "application/xml" if not specified. 229 func (c *Context) RenderXML(data interface{}) error { 230 if err := c.setData(data); err != nil { 231 return c.errorWithLine(err) 232 } 233 c.setContentTypeIfNotExists("application/xml") 234 buf, err := xml.Marshal(c.Data) 235 if err != nil { 236 return c.errorWithLine(err) 237 } 238 if err := c.render(bytes.NewReader(buf)); err != nil { 239 return c.errorWithLine(err) 240 } 241 return nil 242 } 243 244 // RenderText renders the content. 245 // 246 // ContentType set to "text/plain" if not specified. 247 func (c *Context) RenderText(content string) error { 248 c.setContentTypeIfNotExists("text/plain") 249 if err := c.render(strings.NewReader(content)); err != nil { 250 return c.errorWithLine(err) 251 } 252 return nil 253 } 254 255 // RenderError renders an error page with statusCode. 256 // 257 // RenderError is similar to Render, but there is the points where some different. 258 // If err is not nil, RenderError outputs the err to log using c.App.Logger.Error. 259 // RenderError retrieves a template file from statusCode and c.Response.ContentType. 260 // e.g. If statusCode is 500 and ContentType is "application/xml", RenderError will 261 // try to retrieve the template file "errors/500.xml". 262 // If failed to retrieve the template file, it returns result of text with statusCode. 263 // Also ContentType set to "text/html" if not specified. 264 func (c *Context) RenderError(statusCode int, err error, data interface{}) error { 265 if err != nil { 266 c.App.Logger.Error(c.errorWithLine(err)) 267 } 268 if err := c.setData(data); err != nil { 269 return c.errorWithLine(err) 270 } 271 c.setContentTypeIfNotExists("text/html") 272 if err := c.setFormatFromContentTypeIfNotExists(); err != nil { 273 return c.errorWithLine(err) 274 } 275 c.Response.StatusCode = statusCode 276 c.Name = errorTemplateName(statusCode) 277 t, err := c.App.Template.Get(c.App.Config.AppName, c.Layout, c.Name, c.Format) 278 if err != nil { 279 c.Response.ContentType = "text/plain" 280 if err := c.render(bytes.NewReader([]byte(http.StatusText(statusCode)))); err != nil { 281 return c.errorWithLine(err) 282 } 283 return nil 284 } 285 buf := bufPool.Get().(*bytes.Buffer) 286 defer func() { 287 buf.Reset() 288 bufPool.Put(buf) 289 }() 290 if err := t.Execute(buf, c); err != nil { 291 return fmt.Errorf("%s: %v", t.Name(), err) 292 } 293 if err := c.render(buf); err != nil { 294 return c.errorWithLine(err) 295 } 296 return nil 297 } 298 299 // SendFile sends a content. 300 // 301 // The path argument specifies an absolute or relative path. 302 // If absolute path, read the content from the path as it is. 303 // If relative path, First, Try to get the content from included resources and 304 // returns it if successful. Otherwise, Add AppPath and StaticDir to the prefix 305 // of the path and then will read the content from the path that. 306 // Also, set ContentType detect from content if c.Response.ContentType is empty. 307 func (c *Context) SendFile(path string) error { 308 var file io.ReadSeeker 309 path = filepath.FromSlash(path) 310 if rc := c.App.ResourceSet.Get(path); rc != nil { 311 switch b := rc.(type) { 312 case string: 313 file = strings.NewReader(b) 314 case []byte: 315 file = bytes.NewReader(b) 316 } 317 } 318 if file == nil { 319 if !filepath.IsAbs(path) { 320 path = filepath.Join(c.App.Config.AppPath, StaticDir, path) 321 } 322 if _, err := os.Stat(path); err != nil { 323 if err := c.RenderError(http.StatusNotFound, nil, nil); err != nil { 324 return c.errorWithLine(err) 325 } 326 return nil 327 } 328 f, err := os.Open(path) 329 if err != nil { 330 return c.errorWithLine(err) 331 } 332 defer f.Close() 333 file = f 334 } 335 c.Response.ContentType = mime.TypeByExtension(filepath.Ext(path)) 336 if c.Response.ContentType == "" { 337 ct, err := c.detectContentTypeByBody(file) 338 if err != nil { 339 return err 340 } 341 c.Response.ContentType = ct 342 } 343 if err := c.render(file); err != nil { 344 return c.errorWithLine(err) 345 } 346 return nil 347 } 348 349 // Redirect renders result of redirect. 350 // 351 // If permanently is true, redirect to url with 301. (http.StatusMovedPermanently) 352 // Otherwise redirect to url with 302. (http.StatusFound) 353 func (c *Context) Redirect(url string, permanently bool) error { 354 if permanently { 355 c.Response.StatusCode = http.StatusMovedPermanently 356 } else { 357 c.Response.StatusCode = http.StatusFound 358 } 359 http.Redirect(c.Response, c.Request.Request, url, c.Response.StatusCode) 360 return nil 361 } 362 363 // Invoke is shorthand of c.App.Invoke. 364 func (c *Context) Invoke(unit Unit, newFunc func(), defaultFunc func()) { 365 c.App.Invoke(unit, newFunc, defaultFunc) 366 } 367 368 // ErrorWithLine returns error that added the filename and line to err. 369 func (c *Context) ErrorWithLine(err error) error { 370 return c.errorWithLine(err) 371 } 372 373 func (c *Context) render(r io.Reader) error { 374 c.Response.Header().Set("Content-Type", c.Response.ContentType) 375 c.Response.WriteHeader(c.Response.StatusCode) 376 _, err := io.Copy(c.Response, r) 377 return err 378 } 379 380 func (c *Context) detectContentTypeByBody(r io.Reader) (string, error) { 381 buf := make([]byte, 512) 382 if n, err := io.ReadFull(r, buf); err != nil { 383 if err != io.EOF && err != io.ErrUnexpectedEOF { 384 return "", err 385 } 386 buf = buf[:n] 387 } 388 if rs, ok := r.(io.Seeker); ok { 389 if _, err := rs.Seek(0, os.SEEK_SET); err != nil { 390 return "", err 391 } 392 } 393 return http.DetectContentType(buf), nil 394 } 395 396 func (c *Context) setContentTypeIfNotExists(contentType string) { 397 if c.Response.ContentType == "" { 398 c.Response.ContentType = contentType 399 } 400 } 401 402 func (c *Context) setData(data interface{}) error { 403 if data == nil { 404 return nil 405 } 406 srcType := reflect.TypeOf(data) 407 if c.Data == nil || srcType.Kind() != reflect.Map { 408 c.Data = data 409 return nil 410 } 411 destType := reflect.TypeOf(c.Data) 412 if sk, dk := srcType.Key(), destType.Key(); !sk.AssignableTo(dk) { 413 return fmt.Errorf("kocha: context: key of type %v is not assignable to type %v", sk, dk) 414 } 415 src := reflect.ValueOf(data) 416 dest := reflect.ValueOf(c.Data) 417 dtype := destType.Elem() 418 for _, k := range src.MapKeys() { 419 v := src.MapIndex(k) 420 if vtype := v.Type(); !vtype.AssignableTo(dtype) { 421 if !vtype.ConvertibleTo(dtype) { 422 return fmt.Errorf("kocha: context: value of type %v is not assignable to type %v", vtype, dtype) 423 } 424 v = v.Convert(dtype) 425 } 426 dest.SetMapIndex(k, v) 427 } 428 return nil 429 } 430 431 func (c *Context) setFormatFromContentTypeIfNotExists() error { 432 if c.Format != "" { 433 return nil 434 } 435 if c.Format = MimeTypeFormats.Get(c.Response.ContentType); c.Format == "" { 436 return fmt.Errorf("kocha: unknown Content-Type: %v", c.Response.ContentType) 437 } 438 return nil 439 } 440 441 func (c *Context) newParams() *Params { 442 if c.Request.Form == nil { 443 c.Request.Form = url.Values{} 444 } 445 return newParams(c, c.Request.Form, "") 446 } 447 448 func (c *Context) errorWithLine(err error) error { 449 return errorWithLine(err, 3) 450 } 451 452 func (c *Context) reset() { 453 c.Name = "" 454 c.Format = "" 455 c.Data = nil 456 c.Params = nil 457 c.Session = nil 458 c.Flash = nil 459 } 460 461 func (c *Context) reuse() { 462 c.Params.reuse() 463 c.Request.reuse() 464 contextPool.Put(c) 465 } 466 467 // StaticServe is generic controller for serve a static file. 468 type StaticServe struct { 469 *DefaultController 470 } 471 472 func (ss *StaticServe) GET(c *Context) error { 473 path, err := url.Parse(c.Params.Get("path")) 474 if err != nil { 475 return c.RenderError(http.StatusBadRequest, err, nil) 476 } 477 return c.SendFile(path.Path) 478 } 479 480 var internalServerErrorController = &ErrorController{ 481 StatusCode: http.StatusInternalServerError, 482 } 483 484 // ErrorController is generic controller for error response. 485 type ErrorController struct { 486 *DefaultController 487 488 StatusCode int 489 } 490 491 func (ec *ErrorController) GET(c *Context) error { 492 return c.RenderError(ec.StatusCode, nil, nil) 493 }