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  }