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  }