The Revel makes it easy to build web applications using the Model-View-Controller(MVC) pattern by relying on conventions that require a certain structure in your application.
There is a explanation about MVC in Revel official site:
- Models are the essential data objects that describe your application domain. Models also contain
- domain-specific logic for querying and updating the data. Views describe how data is presented and manipulated. In our case, this is the template that is used to present data and controls to the user.
- Controllers handle the request execution. They perform the user’s desired action, they decide which View to display, and they prepare and provide the necessary data to the View for rendering.
Concepts
In order to explain the controller, we must introduce some concepts firstly.
Flash
Flash represents a cookie that gets overwritten on each request. It allows data to be stored across one page at a time. This is commonly used to implement success or error messages.
type Flash struct {
Data, Out map[string]string
}
Before a request, it is generated with the cookies in the request.
func (p FlashPlugin) BeforeRequest(c *Controller) {
c.Flash = restoreFlash(c.Request.Request)
c.RenderArgs["flash"] = c.Flash.Data
}
func restoreFlash(req *http.Request) Flash {
flash := Flash{
Data: make(map[string]string),
Out: make(map[string]string),
}
if cookie, err := req.Cookie(CookiePrefix + "_FLASH"); err == nil {
ParseKeyValueCookie(cookie.Value, func(key, val string) {
flash.Data[key] = val
})
}
return flash
}
And after the request, set the cookies according to the output map.
func (p FlashPlugin) AfterRequest(c *Controller) {
// Store the flash.
var flashValue string
for key, value := range c.Flash.Out {
flashValue += "\x00" + key + ":" + value + "\x00"
}
c.SetCookie(&http.Cookie{
Name: CookiePrefix + "_FLASH",
Value: url.QueryEscape(flashValue),
Path: "/",
})
}
By the way, the flash predefine two methods that export msg to output map.
They are Error
and Success
.
func (f Flash) Error(msg string, args ...interface{}) {
if len(args) == 0 {
f.Out["error"] = msg
} else {
f.Out["error"] = fmt.Sprintf(msg, args...)
}
}
func (f Flash) Success(msg string, args ...interface{}) {
if len(args) == 0 {
f.Out["success"] = msg
} else {
f.Out["success"] = fmt.Sprintf(msg, args...)
}
}
Session
Session is same as the flash, omit it.
Request
A http request is abstracted to a Request
structure:
type Request struct {
*http.Request
ContentType string
Format string // "html", "xml", "json", or "text"
}
A embedding struct of *http.Request
adding ContentType
and Format
attribute.
There is method to new a Request:
func NewRequest(r *http.Request) *Request {
return &Request{
Request: r,
ContentType: ResolveContentType(r),
Format: ResolveFormat(r),
}
}
ResolveContentType
is to extract the content type from the http header.
// Get the content type.
// e.g. From "multipart/form-data; boundary=--" to "multipart/form-data"
// If none is specified, returns "text/html" by default.
func ResolveContentType(req *http.Request) string {
contentType := req.Header.Get("Content-Type")
if contentType == "" {
return "text/html"
}
return strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
}
ResolveFormat
is same as the ResolveContentType
.
func ResolveFormat(req *http.Request) string {
accept := req.Header.Get("accept")
switch {
case accept == "",
strings.HasPrefix(accept, "*/*"),
strings.Contains(accept, "application/xhtml"),
strings.Contains(accept, "text/html"):
return "html"
case strings.Contains(accept, "application/xml"),
strings.Contains(accept, "text/xml"):
return "xml"
case strings.Contains(accept, "text/plain"):
return "txt"
case strings.Contains(accept, "application/json"),
strings.Contains(accept, "text/javascript"):
return "json"
}
return "html"
}
Response
There is also a Response struct to abstract the http response.
type Response struct {
Status int
ContentType string
Out http.ResponseWriter
}
And a new method to create it.
func NewResponse(w http.ResponseWriter) *Response {
return &Response{Out: w}
}
A method to set Status
and ContentType
fields:
// Write the header (for now, just the status code).
// The status may be set directly by the application (c.Response.Status = 501).
// if it isn't, then fall back to the provided status code.
func (resp *Response) WriteHeader(defaultStatusCode int, defaultContentType string) {
if resp.Status == 0 {
resp.Status = defaultStatusCode
}
if resp.ContentType == "" {
resp.ContentType = defaultContentType
}
resp.Out.Header().Set("Content-Type", resp.ContentType)
resp.Out.WriteHeader(resp.Status)
}
Management
To manage all kinds of controllers, Revel use ControllerType
to express a controller instance in
his internal bookkeeping.
type ControllerType struct {
Type reflect.Type
Methods []*MethodType
}
type MethodType struct {
Name string
Args []*MethodArg
RenderArgNames map[int][]string
lowerName string
}
type MethodArg struct {
Name string
Type reflect.Type
}
To see whether a method is belong a controller, ControllerType
export a Method
method.
// Searches for a given exported method (case insensitive)
func (ct *ControllerType) Method(name string) *MethodType {
lowerName := strings.ToLower(name)
for _, method := range ct.Methods {
if method.lowerName == lowerName {
return method
}
}
return nil
}
just compare their names.
And all ControllerType
are collected in a map variable controllers
. If you want to add a new
controller, use the RegisterController
function to do it.
// Register a Controller and its Methods with Revel.
func RegisterController(c interface{}, methods []*MethodType) {
// De-star the controller type
// (e.g. given TypeOf((*Application)(nil)), want TypeOf(Application))
var t reflect.Type = reflect.TypeOf(c)
var elem reflect.Type = t.Elem()
// De-star all of the method arg types too.
for _, m := range methods {
m.lowerName = strings.ToLower(m.Name)
for _, arg := range m.Args {
arg.Type = arg.Type.Elem()
}
}
controllers[strings.ToLower(elem.Name())] = &ControllerType{Type: elem, Methods: methods}
TRACE.Printf("Registered controller: %s", elem.Name())
}
- Note: the controller itself and methods are all pointers, so we must dereference them at first.
There is also a helper function LookupControllerType
to find a controller in the map.
func LookupControllerType(name string) *ControllerType {
return controllers[strings.ToLower(name)]
}
When user need create their own controllers, it may inherit revel base controller. So revel used two abstractions to express them respectively.
A user controller may like this:
type AppController struct {
*rev.Controller
...
//some other user fields
}
And rev.Controller
we will talk it later. NewAppController
function will create a
AppController
, Note that you must firstly register the controller, then you can create it.
func NewAppController(req *Request, resp *Response, controllerName, methodName string) (*Controller, reflect.Value) {
var appControllerType *ControllerType = LookupControllerType(controllerName)
if appControllerType == nil {
INFO.Printf("Controller %s not found: %s", controllerName, req.URL)
return nil, reflect.ValueOf(nil)
}
controller := NewController(req, resp, appControllerType)
appControllerPtr := initNewAppController(appControllerType.Type, controller)
And initNewAppController
is a helper that initializes (zeros) a new app controller value.
Generally, everything is set to its zero value, except:
- Embedded controller pointers are newed up.
- The rev.Controller embedded type is set to the value provided.
func initNewAppController(appControllerType reflect.Type, c *Controller) reflect.Value {
// It might be a multi-level embedding, so we have to create new controllers
// at every level of the hierarchy.
// ASSUME: the first field in each type is the way up to rev.Controller.
appControllerPtr := reflect.New(appControllerType)
ptr := appControllerPtr
for {
var (
embeddedField reflect.Value = ptr.Elem().Field(0)
embeddedFieldType reflect.Type = embeddedField.Type()
)
// Check if it's the controller.
if embeddedFieldType == controllerType {
embeddedField.Set(reflect.ValueOf(c).Elem())
break
} else if embeddedFieldType == controllerPtrType {
embeddedField.Set(reflect.ValueOf(c))
break
}
// If the embedded field is a pointer, then instantiate an object and set it.
// (If it's not a pointer, then it's already initialized)
if embeddedFieldType.Kind() == reflect.Ptr {
embeddedField.Set(reflect.New(embeddedFieldType.Elem()))
ptr = embeddedField
} else {
ptr = embeddedField.Addr()
}
}
return appControllerPtr
}
Here, there is assume that the rev.Controller
or *rev.Controller
is always lays at the first
field of the container, no matter how many levels it belongs. Fields initialed a zero value if it is
not the destination.
Structure - Result
A Result express a http response usually.
type Result interface {
Apply(req *Request, resp *Response)
}
Structure - Controller
Controller is the context for the request. It contains the request and responese data. So it the central structure of the netflow.
type Controller struct {
Name string
Type *ControllerType
MethodType *MethodType
Request *Request
Response *Response
Flash Flash // User cookie, cleared after each request.
Session Session // Session, stored in cookie, signed.
Params *Params // Parameters from URL and form (including multipart).
Args map[string]interface{} // Per-request scratch space.
RenderArgs map[string]interface{} // Args passed to the template.
Validation *Validation // Data validation helpers
Txn *sql.Tx // Nil by default, but may be used by the app / plugins
}
Some fields we don’t talk today, we are concern the structure itself now.
Method - NewController
func NewController(req *Request, resp *Response, ct *ControllerType) *Controller {
return &Controller{
Name: ct.Type.Name(),
Type: ct,
Request: req,
Response: resp,
Params: ParseParams(req),
Args: map[string]interface{}{},
RenderArgs: map[string]interface{}{
"RunMode": RunMode,
},
}
}
ParseParams
is to extract the parameters from the request.
Method - Invoke
Invoke the given method, save headers/cookies to the response, and apply the. Definition is here.
func (c *Controller) Invoke(appControllerPtr reflect.Value, method reflect.Value, methodArgs []reflect.Value)
Firstly, register two defer functions. One for handle panic and one for clean up some temporary stuffs.
// Handle panics.
defer func() {
if err := recover(); err != nil {
handleInvocationPanic(c, err)
}
}()
// Clean up from the request.
defer func() {
// Delete temp files.
if c.Request.MultipartForm != nil {
err := c.Request.MultipartForm.RemoveAll()
if err != nil {
WARN.Println("Error removing temporary files:", err)
}
}
for _, tmpFile := range c.Params.tmpFiles {
err := os.Remove(tmpFile.Name())
if err != nil {
WARN.Println("Could not remove upload temp file:", err)
}
}
}()
Then the sequence is:
// Run the plugins.
plugins.BeforeRequest(c)
// Calculate the Result by running the interceptors and the action.
resultValue := func() reflect.Value {
// Call the BEFORE interceptors
result := c.invokeInterceptors(BEFORE, appControllerPtr)
if result != nil {
return reflect.ValueOf(result)
}
// Invoke the action.
resultValue := method.Call(methodArgs)[0]
// Call the AFTER interceptors
result = c.invokeInterceptors(AFTER, appControllerPtr)
if result != nil {
return reflect.ValueOf(result)
}
return resultValue
}()
plugins.AfterRequest(c)
if resultValue.Kind() == reflect.Interface && resultValue.IsNil() {
return
}
result := resultValue.Interface().(Result)
// Apply the result, which generally results in the ResponseWriter getting written.
result.Apply(c.Request, c.Response)
So the work flow is like this:
Method - Render
Render a template corresponding to the calling Controller method.
At first, it get method itself and save it in controller.MethodType
.
func (c *Controller) Render(extraRenderArgs ...interface{}) Result {
// Get the calling function name.
pc, _, line, ok := runtime.Caller(1)
if !ok {
ERROR.Println("Failed to get Caller information")
return nil
}
// e.g. sample/app/controllers.(*Application).Index
var fqViewName string = runtime.FuncForPC(pc).Name()
var viewName string = fqViewName[strings.LastIndex(fqViewName, ".")+1 : len(fqViewName)]
// Determine what method we are in.
// (e.g. the invoked controller method might have delegated to another method)
methodType := c.MethodType
if methodType.Name != viewName {
methodType = c.Type.Method(viewName)
if methodType == nil {
return c.RenderError(fmt.Errorf(
"No Method %s in Controller %s when loading the view."+
" (delegating Render is only supported within the same controller)",
viewName, c.Name))
}
}
Then we should set the render variables. Because the Render method may be called from different places, we use line number to distinguish each other.
// Get the extra RenderArgs passed in.
if renderArgNames, ok := methodType.RenderArgNames[line]; ok {
if len(renderArgNames) == len(extraRenderArgs) {
for i, extraRenderArg := range extraRenderArgs {
c.RenderArgs[renderArgNames[i]] = extraRenderArg
}
} else {
ERROR.Println(len(renderArgNames), "RenderArg names found for",
len(extraRenderArgs), "extra RenderArgs")
}
} else {
ERROR.Println("No RenderArg names found for Render call on line", line,
"(Method", methodType, ", ViewName", viewName, ")")
}
return c.RenderTemplate(c.Name + "/" + viewName + ".html")
Method - RenderTemplate
A less magical way to render a template. Renders the given template, using the current controller.RenderArgs
.
func (c *Controller) RenderTemplate(templatePath string) Result {
// Get the Template.
template, err := MainTemplateLoader.Template(templatePath)
if err != nil {
return c.RenderError(err)
}
return &RenderTemplateResult{
Template: template,
RenderArgs: c.RenderArgs,
}
}
The request template is loaded by a templateLoader
.
Method - RenderJson
Same as RenderTemplate
// Uses encoding/json.Marshal to return JSON to the client.
func (c *Controller) RenderJson(o interface{}) Result {
return RenderJsonResult{o}
}
Method - RenderXml
Same as RenderTemplate
// Uses encoding/xml.Marshal to return XML to the client.
func (c *Controller) RenderXml(o interface{}) Result {
return RenderXmlResult{o}
}
Method - RenderText
Same as RenderTemplate
// Render plaintext in response, printf style.
func (c *Controller) RenderText(text string, objs ...interface{}) Result {
finalText := text
if len(objs) > 0 {
finalText = fmt.Sprintf(text, objs)
}
return &RenderTextResult{finalText}
}
Method - Todo
Same as RenderTemplate
// Render a "todo" indicating that the action isn't done yet.
func (c *Controller) Todo() Result {
c.Response.Status = http.StatusNotImplemented
return c.RenderError(&Error{
Title: "TODO",
Description: "This action is not implemented",
})
}
Method - NotFound
Same as RenderTemplate
func (c *Controller) NotFound(msg string) Result {
c.Response.Status = http.StatusNotFound
return c.RenderError(&Error{
Title: "Not Found",
Description: msg,
})
}
Method - RenderFile
Return a file, either displayed inline or downloaded as an attachment. The name and size are taken from the file info.
func (c *Controller) RenderFile(file *os.File, delivery ContentDisposition) Result {
var length int64 = -1
fileInfo, err := file.Stat()
if err != nil {
WARN.Println("RenderFile error:", err)
}
if fileInfo != nil {
length = fileInfo.Size()
}
return &BinaryResult{
Reader: file,
Name: filepath.Base(file.Name()),
Length: length,
Delivery: delivery,
}
}
Method - Redirect
Redirect to an action or to a URL.
func (c *Controller) Redirect(val interface{}, args ...interface{}) Result {
if url, ok := val.(string); ok {
if len(args) == 0 {
return &RedirectToUrlResult{url}
}
return &RedirectToUrlResult{fmt.Sprintf(url, args...)}
}
return &RedirectToActionResult{val}
}