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