diff --git a/.travis.yml b/.travis.yml index 380eca81..3c821dcd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,11 @@ env: - ORM_DRIVER=sqlite3 ORM_SOURCE=$TRAVIS_BUILD_DIR/orm_test.db - ORM_DRIVER=mysql ORM_SOURCE="root:@/orm_test?charset=utf8" - ORM_DRIVER=postgres ORM_SOURCE="user=postgres dbname=orm_test sslmode=disable" +before_install: + - git clone git://github.com/ideawu/ssdb.git + - cd ssdb + - make + - cd .. install: - go get github.com/lib/pq - go get github.com/go-sql-driver/mysql @@ -28,10 +33,16 @@ install: - go get github.com/siddontang/ledisdb/ledis - go get golang.org/x/tools/cmd/vet - go get github.com/golang/lint/golint + - go get github.com/ssdb/gossdb/ssdb before_script: - sh -c "if [ '$ORM_DRIVER' = 'postgres' ]; then psql -c 'create database orm_test;' -U postgres; fi" - sh -c "if [ '$ORM_DRIVER' = 'mysql' ]; then mysql -u root -e 'create database orm_test;'; fi" - sh -c "if [ '$ORM_DRIVER' = 'sqlite' ]; then touch $TRAVIS_BUILD_DIR/orm_test.db; fi" + - mkdir -p res/var + - ./ssdb/ssdb-server ./ssdb/ssdb.conf -d +after_script: + -killall -w ssdb-server + - rm -rf ./res/var/* script: - go vet -x ./... - $HOME/gopath/bin/golint ./... diff --git a/beego.go b/beego.go index fb628e5f..8f82cdcf 100644 --- a/beego.go +++ b/beego.go @@ -23,7 +23,7 @@ import ( const ( // VERSION represent beego web framework version. - VERSION = "1.6.0" + VERSION = "1.6.1" // DEV is for develop DEV = "dev" diff --git a/cache/ssdb/ssdb.go b/cache/ssdb/ssdb.go new file mode 100644 index 00000000..bfee69ce --- /dev/null +++ b/cache/ssdb/ssdb.go @@ -0,0 +1,240 @@ +package ssdb + +import ( + "encoding/json" + "errors" + "strconv" + "strings" + "time" + + "github.com/ssdb/gossdb/ssdb" + + "github.com/astaxie/beego/cache" +) + +// Cache SSDB adapter +type Cache struct { + conn *ssdb.Client + conninfo []string +} + +//NewSsdbCache create new ssdb adapter. +func NewSsdbCache() cache.Cache { + return &Cache{} +} + +// Get get value from memcache. +func (rc *Cache) Get(key string) interface{} { + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return nil + } + } + value, err := rc.conn.Get(key) + if err == nil { + return value + } + return nil +} + +// GetMulti get value from memcache. +func (rc *Cache) GetMulti(keys []string) []interface{} { + size := len(keys) + var values []interface{} + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + for i := 0; i < size; i++ { + values = append(values, err) + } + return values + } + } + res, err := rc.conn.Do("multi_get", keys) + resSize := len(res) + if err == nil { + for i := 1; i < resSize; i += 2 { + values = append(values, string(res[i+1])) + } + return values + } + for i := 0; i < size; i++ { + values = append(values, err) + } + return values +} + +// DelMulti get value from memcache. +func (rc *Cache) DelMulti(keys []string) error { + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return err + } + } + _, err := rc.conn.Do("multi_del", keys) + if err != nil { + return err + } + return nil +} + +// Put put value to memcache. only support string. +func (rc *Cache) Put(key string, value interface{}, timeout time.Duration) error { + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return err + } + } + v, ok := value.(string) + if !ok { + return errors.New("value must string") + } + var resp []string + var err error + ttl := int(timeout / time.Second) + if ttl < 0 { + resp, err = rc.conn.Do("set", key, v) + } else { + resp, err = rc.conn.Do("setx", key, v, ttl) + } + if err != nil { + return err + } + if len(resp) == 2 && resp[0] == "ok" { + return nil + } + return errors.New("bad response") +} + +// Delete delete value in memcache. +func (rc *Cache) Delete(key string) error { + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return err + } + } + _, err := rc.conn.Del(key) + if err != nil { + return err + } + return nil +} + +// Incr increase counter. +func (rc *Cache) Incr(key string) error { + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return err + } + } + _, err := rc.conn.Do("incr", key, 1) + return err +} + +// Decr decrease counter. +func (rc *Cache) Decr(key string) error { + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return err + } + } + _, err := rc.conn.Do("incr", key, -1) + return err +} + +// IsExist check value exists in memcache. +func (rc *Cache) IsExist(key string) bool { + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return false + } + } + resp, err := rc.conn.Do("exists", key) + if err != nil { + return false + } + if resp[1] == "1" { + return true + } + return false + +} + +// ClearAll clear all cached in memcache. +func (rc *Cache) ClearAll() error { + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return err + } + } + keyStart, keyEnd, limit := "", "", 50 + resp, err := rc.Scan(keyStart, keyEnd, limit) + for err == nil { + size := len(resp) + if size == 1 { + return nil + } + keys := []string{} + for i := 1; i < size; i += 2 { + keys = append(keys, string(resp[i])) + } + _, e := rc.conn.Do("multi_del", keys) + if e != nil { + return e + } + keyStart = resp[size-2] + resp, err = rc.Scan(keyStart, keyEnd, limit) + } + return err +} + +// Scan key all cached in ssdb. +func (rc *Cache) Scan(keyStart string, keyEnd string, limit int) ([]string, error) { + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return nil, err + } + } + resp, err := rc.conn.Do("scan", keyStart, keyEnd, limit) + if err != nil { + return nil, err + } + return resp, nil +} + +// StartAndGC start memcache adapter. +// config string is like {"conn":"connection info"}. +// if connecting error, return. +func (rc *Cache) StartAndGC(config string) error { + var cf map[string]string + json.Unmarshal([]byte(config), &cf) + if _, ok := cf["conn"]; !ok { + return errors.New("config has no conn key") + } + rc.conninfo = strings.Split(cf["conn"], ";") + if rc.conn == nil { + if err := rc.connectInit(); err != nil { + return err + } + } + return nil +} + +// connect to memcache and keep the connection. +func (rc *Cache) connectInit() error { + conninfoArray := strings.Split(rc.conninfo[0], ":") + host := conninfoArray[0] + port, e := strconv.Atoi(conninfoArray[1]) + if e != nil { + return e + } + var err error + rc.conn, err = ssdb.Connect(host, port) + if err != nil { + return err + } + return nil +} + +func init() { + cache.Register("ssdb", NewSsdbCache) +} diff --git a/cache/ssdb/ssdb_test.go b/cache/ssdb/ssdb_test.go new file mode 100644 index 00000000..e03ba343 --- /dev/null +++ b/cache/ssdb/ssdb_test.go @@ -0,0 +1,103 @@ +package ssdb + +import ( + "github.com/astaxie/beego/cache" + "strconv" + "testing" + "time" +) + +func TestSsdbcacheCache(t *testing.T) { + ssdb, err := cache.NewCache("ssdb", `{"conn": "127.0.0.1:8888"}`) + if err != nil { + t.Error("init err") + } + + // test put and exist + if ssdb.IsExist("ssdb") { + t.Error("check err") + } + timeoutDuration := 10 * time.Second + //timeoutDuration := -10*time.Second if timeoutDuration is negtive,it means permanent + if err = ssdb.Put("ssdb", "ssdb", timeoutDuration); err != nil { + t.Error("set Error", err) + } + if !ssdb.IsExist("ssdb") { + t.Error("check err") + } + + // Get test done + if err = ssdb.Put("ssdb", "ssdb", timeoutDuration); err != nil { + t.Error("set Error", err) + } + + if v := ssdb.Get("ssdb"); v != "ssdb" { + t.Error("get Error") + } + + //inc/dec test done + if err = ssdb.Put("ssdb", "2", timeoutDuration); err != nil { + t.Error("set Error", err) + } + if err = ssdb.Incr("ssdb"); err != nil { + t.Error("incr Error", err) + } + + if v, err := strconv.Atoi(ssdb.Get("ssdb").(string)); err != nil || v != 3 { + t.Error("get err") + } + + if err = ssdb.Decr("ssdb"); err != nil { + t.Error("decr error") + } + + // test del + if err = ssdb.Put("ssdb", "3", timeoutDuration); err != nil { + t.Error("set Error", err) + } + if v, err := strconv.Atoi(ssdb.Get("ssdb").(string)); err != nil || v != 3 { + t.Error("get err") + } + if err := ssdb.Delete("ssdb"); err == nil { + if ssdb.IsExist("ssdb") { + t.Error("delete err") + } + } + + //test string + if err = ssdb.Put("ssdb", "ssdb", -10*time.Second); err != nil { + t.Error("set Error", err) + } + if !ssdb.IsExist("ssdb") { + t.Error("check err") + } + if v := ssdb.Get("ssdb").(string); v != "ssdb" { + t.Error("get err") + } + + //test GetMulti done + if err = ssdb.Put("ssdb1", "ssdb1", -10*time.Second); err != nil { + t.Error("set Error", err) + } + if !ssdb.IsExist("ssdb1") { + t.Error("check err") + } + vv := ssdb.GetMulti([]string{"ssdb", "ssdb1"}) + if len(vv) != 2 { + t.Error("getmulti error") + } + if vv[0].(string) != "ssdb" { + t.Error("getmulti error") + } + if vv[1].(string) != "ssdb1" { + t.Error("getmulti error") + } + + // test clear all done + if err = ssdb.ClearAll(); err != nil { + t.Error("clear all err") + } + if ssdb.IsExist("ssdb") || ssdb.IsExist("ssdb1") { + t.Error("check err") + } +} diff --git a/config.go b/config.go index 42b35fd2..2761e7cb 100644 --- a/config.go +++ b/config.go @@ -16,7 +16,6 @@ package beego import ( "fmt" - "html/template" "os" "path/filepath" "strings" @@ -106,8 +105,6 @@ var ( AppConfig *beegoAppConfig // AppPath is the absolute path to the app AppPath string - // TemplateCache stores template caching - TemplateCache map[string]*template.Template // GlobalSessions is the instance for the session manager GlobalSessions *session.Manager @@ -188,7 +185,9 @@ func init() { return } - parseConfig(appConfigPath) + if err := parseConfig(appConfigPath); err != nil { + panic(err) + } } // now only support ini, next will support json. diff --git a/config/fake.go b/config/fake.go index 6daaca2c..7e362608 100644 --- a/config/fake.go +++ b/config/fake.go @@ -46,12 +46,16 @@ func (c *fakeConfigContainer) DefaultString(key string, defaultval string) strin } func (c *fakeConfigContainer) Strings(key string) []string { - return strings.Split(c.getData(key), ";") + v := c.getData(key) + if v == "" { + return nil + } + return strings.Split(v, ";") } func (c *fakeConfigContainer) DefaultStrings(key string, defaultval []string) []string { v := c.Strings(key) - if len(v) == 0 { + if v == nil { return defaultval } return v diff --git a/config/ini.go b/config/ini.go index da6f2b3a..9c19b9b1 100644 --- a/config/ini.go +++ b/config/ini.go @@ -269,15 +269,20 @@ func (c *IniConfigContainer) DefaultString(key string, defaultval string) string } // Strings returns the []string value for a given key. +// Return nil if config value does not exist or is empty. func (c *IniConfigContainer) Strings(key string) []string { - return strings.Split(c.String(key), ";") + v := c.String(key) + if v == "" { + return nil + } + return strings.Split(v, ";") } // DefaultStrings returns the []string value for a given key. // if err != nil return defaltval func (c *IniConfigContainer) DefaultStrings(key string, defaultval []string) []string { v := c.Strings(key) - if len(v) == 0 { + if v == nil { return defaultval } return v diff --git a/config/ini_test.go b/config/ini_test.go index 11063d99..93fce61f 100644 --- a/config/ini_test.go +++ b/config/ini_test.go @@ -71,6 +71,7 @@ peers = one;two;three "null": "", "demo2::key1": "", "error": "", + "emptystrings": []string{}, } ) diff --git a/config/json.go b/config/json.go index 0bc1d456..fce517eb 100644 --- a/config/json.go +++ b/config/json.go @@ -173,7 +173,7 @@ func (c *JSONConfigContainer) DefaultString(key string, defaultval string) strin func (c *JSONConfigContainer) Strings(key string) []string { stringVal := c.String(key) if stringVal == "" { - return []string{} + return nil } return strings.Split(c.String(key), ";") } @@ -181,7 +181,7 @@ func (c *JSONConfigContainer) Strings(key string) []string { // DefaultStrings returns the []string value for a given key. // if err != nil return defaltval func (c *JSONConfigContainer) DefaultStrings(key string, defaultval []string) []string { - if v := c.Strings(key); len(v) > 0 { + if v := c.Strings(key); v != nil { return v } return defaultval diff --git a/config/xml/xml.go b/config/xml/xml.go index ffb32862..b5291bf4 100644 --- a/config/xml/xml.go +++ b/config/xml/xml.go @@ -174,14 +174,18 @@ func (c *ConfigContainer) DefaultString(key string, defaultval string) string { // Strings returns the []string value for a given key. func (c *ConfigContainer) Strings(key string) []string { - return strings.Split(c.String(key), ";") + v := c.String(key) + if v == "" { + return nil + } + return strings.Split(v, ";") } // DefaultStrings returns the []string value for a given key. // if err != nil return defaltval func (c *ConfigContainer) DefaultStrings(key string, defaultval []string) []string { v := c.Strings(key) - if len(v) == 0 { + if v == nil { return defaultval } return v diff --git a/config/xml/xml_test.go b/config/xml/xml_test.go index fa3c17f1..60dcba54 100644 --- a/config/xml/xml_test.go +++ b/config/xml/xml_test.go @@ -82,4 +82,7 @@ func TestXML(t *testing.T) { if xmlconf.String("name") != "astaxie" { t.Fatal("get name error") } + if xmlconf.Strings("emptystrings") != nil { + t.Fatal("get emtpy strings error") + } } diff --git a/config/yaml/yaml.go b/config/yaml/yaml.go index 9a96ac92..7e1d0426 100644 --- a/config/yaml/yaml.go +++ b/config/yaml/yaml.go @@ -211,14 +211,18 @@ func (c *ConfigContainer) DefaultString(key string, defaultval string) string { // Strings returns the []string value for a given key. func (c *ConfigContainer) Strings(key string) []string { - return strings.Split(c.String(key), ";") + v := c.String(key) + if v == "" { + return nil + } + return strings.Split(v, ";") } // DefaultStrings returns the []string value for a given key. // if err != nil return defaltval func (c *ConfigContainer) DefaultStrings(key string, defaultval []string) []string { v := c.Strings(key) - if len(v) == 0 { + if v == nil { return defaultval } return v diff --git a/config/yaml/yaml_test.go b/config/yaml/yaml_test.go index 19ecdca1..80cbb8fe 100644 --- a/config/yaml/yaml_test.go +++ b/config/yaml/yaml_test.go @@ -79,4 +79,8 @@ func TestYaml(t *testing.T) { if yamlconf.String("name") != "astaxie" { t.Fatal("get name error") } + + if yamlconf.Strings("emptystrings") != nil { + t.Fatal("get emtpy strings error") + } } diff --git a/context/context.go b/context/context.go index db790ff2..63a1313d 100644 --- a/context/context.go +++ b/context/context.go @@ -24,11 +24,13 @@ package context import ( "bufio" + "bytes" "crypto/hmac" "crypto/sha1" "encoding/base64" "errors" "fmt" + "io" "net" "net/http" "strconv" @@ -59,7 +61,10 @@ type Context struct { // Reset init Context, BeegoInput and BeegoOutput func (ctx *Context) Reset(rw http.ResponseWriter, r *http.Request) { ctx.Request = r - ctx.ResponseWriter = &Response{rw, false, 0} + if ctx.ResponseWriter == nil { + ctx.ResponseWriter = &Response{} + } + ctx.ResponseWriter.reset(rw) ctx.Input.Reset(ctx) ctx.Output.Reset(ctx) } @@ -176,25 +181,43 @@ type Response struct { Status int } +func (r *Response) reset(rw http.ResponseWriter) { + r.ResponseWriter = rw + r.Status = 0 + r.Started = false +} + // Write writes the data to the connection as part of an HTTP reply, // and sets `started` to true. // started means the response has sent out. -func (w *Response) Write(p []byte) (int, error) { - w.Started = true - return w.ResponseWriter.Write(p) +func (r *Response) Write(p []byte) (int, error) { + r.Started = true + return r.ResponseWriter.Write(p) +} + +// Copy writes the data to the connection as part of an HTTP reply, +// and sets `started` to true. +// started means the response has sent out. +func (r *Response) Copy(buf *bytes.Buffer) (int64, error) { + r.Started = true + return io.Copy(r.ResponseWriter, buf) } // WriteHeader sends an HTTP response header with status code, // and sets `started` to true. -func (w *Response) WriteHeader(code int) { - w.Status = code - w.Started = true - w.ResponseWriter.WriteHeader(code) +func (r *Response) WriteHeader(code int) { + if r.Status > 0 { + //prevent multiple response.WriteHeader calls + return + } + r.Status = code + r.Started = true + r.ResponseWriter.WriteHeader(code) } // Hijack hijacker for http -func (w *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hj, ok := w.ResponseWriter.(http.Hijacker) +func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj, ok := r.ResponseWriter.(http.Hijacker) if !ok { return nil, nil, errors.New("webserver doesn't support hijacking") } @@ -202,15 +225,15 @@ func (w *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { } // Flush http.Flusher -func (w *Response) Flush() { - if f, ok := w.ResponseWriter.(http.Flusher); ok { +func (r *Response) Flush() { + if f, ok := r.ResponseWriter.(http.Flusher); ok { f.Flush() } } // CloseNotify http.CloseNotifier -func (w *Response) CloseNotify() <-chan bool { - if cn, ok := w.ResponseWriter.(http.CloseNotifier); ok { +func (r *Response) CloseNotify() <-chan bool { + if cn, ok := r.ResponseWriter.(http.CloseNotifier); ok { return cn.CloseNotify() } return nil diff --git a/context/output.go b/context/output.go index 2d756e27..17404702 100644 --- a/context/output.go +++ b/context/output.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" "html/template" - "io" "mime" "net/http" "path/filepath" @@ -57,7 +56,7 @@ func (output *BeegoOutput) Header(key, val string) { // Body sets response body content. // if EnableGzip, compress content string. // it sends out response body directly. -func (output *BeegoOutput) Body(content []byte) { +func (output *BeegoOutput) Body(content []byte) error { var encoding string var buf = &bytes.Buffer{} if output.EnableGzip { @@ -75,7 +74,8 @@ func (output *BeegoOutput) Body(content []byte) { output.Status = 0 } - io.Copy(output.Context.ResponseWriter, buf) + _, err := output.Context.ResponseWriter.Copy(buf) + return err } // Cookie sets cookie value via given key. @@ -97,9 +97,10 @@ func (output *BeegoOutput) Cookie(name string, value string, others ...interface maxAge = v } - if maxAge > 0 { + switch { + case maxAge > 0: fmt.Fprintf(&b, "; Expires=%s; Max-Age=%d", time.Now().Add(time.Duration(maxAge)*time.Second).UTC().Format(time.RFC1123), maxAge) - } else { + case maxAge < 0: fmt.Fprintf(&b, "; Max-Age=0") } } @@ -186,8 +187,7 @@ func (output *BeegoOutput) JSON(data interface{}, hasIndent bool, coding bool) e if coding { content = []byte(stringsToJSON(string(content))) } - output.Body(content) - return nil + return output.Body(content) } // JSONP writes jsonp to response body. @@ -212,8 +212,7 @@ func (output *BeegoOutput) JSONP(data interface{}, hasIndent bool) error { callbackContent.WriteString("(") callbackContent.Write(content) callbackContent.WriteString(");\r\n") - output.Body(callbackContent.Bytes()) - return nil + return output.Body(callbackContent.Bytes()) } // XML writes xml string to response body. @@ -230,8 +229,7 @@ func (output *BeegoOutput) XML(data interface{}, hasIndent bool) error { http.Error(output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError) return err } - output.Body(content) - return nil + return output.Body(content) } // Download forces response for download file. diff --git a/controller.go b/controller.go index a2943d42..85894275 100644 --- a/controller.go +++ b/controller.go @@ -185,8 +185,7 @@ func (c *Controller) Render() error { return err } c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8") - c.Ctx.Output.Body(rb) - return nil + return c.Ctx.Output.Body(rb) } // RenderString returns the rendered template string. Do not send out response. @@ -197,33 +196,9 @@ func (c *Controller) RenderString() (string, error) { // RenderBytes returns the bytes of rendered template string. Do not send out response. func (c *Controller) RenderBytes() ([]byte, error) { - //if the controller has set layout, then first get the tplname's content set the content to the layout - var buf bytes.Buffer - if c.Layout != "" { - if c.TplName == "" { - c.TplName = strings.ToLower(c.controllerName) + "/" + strings.ToLower(c.actionName) + "." + c.TplExt - } - - if BConfig.RunMode == DEV { - buildFiles := []string{c.TplName} - if c.LayoutSections != nil { - for _, sectionTpl := range c.LayoutSections { - if sectionTpl == "" { - continue - } - buildFiles = append(buildFiles, sectionTpl) - } - } - BuildTemplate(BConfig.WebConfig.ViewsPath, buildFiles...) - } - if _, ok := BeeTemplates[c.TplName]; !ok { - panic("can't find templatefile in the path:" + c.TplName) - } - err := BeeTemplates[c.TplName].ExecuteTemplate(&buf, c.TplName, c.Data) - if err != nil { - Trace("template Execute err:", err) - return nil, err - } + buf, err := c.renderTemplate() + //if the controller has set layout, then first get the tplName's content set the content to the layout + if err == nil && c.Layout != "" { c.Data["LayoutContent"] = template.HTML(buf.String()) if c.LayoutSections != nil { @@ -232,11 +207,9 @@ func (c *Controller) RenderBytes() ([]byte, error) { c.Data[sectionName] = "" continue } - buf.Reset() - err = BeeTemplates[sectionTpl].ExecuteTemplate(&buf, sectionTpl, c.Data) + err = executeTemplate(&buf, sectionTpl, c.Data) if err != nil { - Trace("template Execute err:", err) return nil, err } c.Data[sectionName] = template.HTML(buf.String()) @@ -244,30 +217,32 @@ func (c *Controller) RenderBytes() ([]byte, error) { } buf.Reset() - err = BeeTemplates[c.Layout].ExecuteTemplate(&buf, c.Layout, c.Data) - if err != nil { - Trace("template Execute err:", err) - return nil, err - } - return buf.Bytes(), nil + executeTemplate(&buf, c.Layout, c.Data) } + return buf.Bytes(), err +} +func (c *Controller) renderTemplate() (bytes.Buffer, error) { + var buf bytes.Buffer if c.TplName == "" { c.TplName = strings.ToLower(c.controllerName) + "/" + strings.ToLower(c.actionName) + "." + c.TplExt } if BConfig.RunMode == DEV { - BuildTemplate(BConfig.WebConfig.ViewsPath, c.TplName) + buildFiles := []string{c.TplName} + if c.Layout != "" { + buildFiles = append(buildFiles, c.Layout) + if c.LayoutSections != nil { + for _, sectionTpl := range c.LayoutSections { + if sectionTpl == "" { + continue + } + buildFiles = append(buildFiles, sectionTpl) + } + } + } + BuildTemplate(BConfig.WebConfig.ViewsPath, buildFiles...) } - if _, ok := BeeTemplates[c.TplName]; !ok { - panic("can't find templatefile in the path:" + c.TplName) - } - buf.Reset() - err := BeeTemplates[c.TplName].ExecuteTemplate(&buf, c.TplName, c.Data) - if err != nil { - Trace("template Execute err:", err) - return nil, err - } - return buf.Bytes(), nil + return buf, executeTemplate(&buf, c.TplName, c.Data) } // Redirect sends the redirection response to url with status code. @@ -286,7 +261,7 @@ func (c *Controller) Abort(code string) { // CustomAbort stops controller handler and show the error data, it's similar Aborts, but support status code and body. func (c *Controller) CustomAbort(status int, body string) { - c.Ctx.ResponseWriter.WriteHeader(status) + c.Ctx.Output.Status = status // first panic from ErrorMaps, is is user defined error functions. if _, ok := ErrorMaps[body]; ok { panic(body) diff --git a/httplib/httplib.go b/httplib/httplib.go index fb64a30a..76984122 100644 --- a/httplib/httplib.go +++ b/httplib/httplib.go @@ -301,13 +301,12 @@ func (b *BeegoHTTPRequest) Body(data interface{}) *BeegoHTTPRequest { // JSONBody adds request raw body encoding by JSON. func (b *BeegoHTTPRequest) JSONBody(obj interface{}) (*BeegoHTTPRequest, error) { if b.req.Body == nil && obj != nil { - buf := bytes.NewBuffer(nil) - enc := json.NewEncoder(buf) - if err := enc.Encode(obj); err != nil { + byts, err := json.Marshal(obj) + if err != nil { return b, err } - b.req.Body = ioutil.NopCloser(buf) - b.req.ContentLength = int64(buf.Len()) + b.req.Body = ioutil.NopCloser(bytes.NewReader(byts)) + b.req.ContentLength = int64(len(byts)) b.req.Header.Set("Content-Type", "application/json") } return b, nil diff --git a/logs/conn.go b/logs/conn.go index 5d78467b..1db1a427 100644 --- a/logs/conn.go +++ b/logs/conn.go @@ -17,7 +17,6 @@ package logs import ( "encoding/json" "io" - "log" "net" "time" ) @@ -25,7 +24,7 @@ import ( // connWriter implements LoggerInterface. // it writes messages in keep-live tcp connection. type connWriter struct { - lg *log.Logger + lg *logWriter innerWriter io.WriteCloser ReconnectOnMsg bool `json:"reconnectOnMsg"` Reconnect bool `json:"reconnect"` @@ -43,8 +42,8 @@ func NewConn() Logger { // Init init connection writer with json config. // json config only need key "level". -func (c *connWriter) Init(jsonconfig string) error { - return json.Unmarshal([]byte(jsonconfig), c) +func (c *connWriter) Init(jsonConfig string) error { + return json.Unmarshal([]byte(jsonConfig), c) } // WriteMsg write message in connection. @@ -53,7 +52,7 @@ func (c *connWriter) WriteMsg(when time.Time, msg string, level int) error { if level > c.Level { return nil } - if c.neddedConnectOnMsg() { + if c.needToConnectOnMsg() { err := c.connect() if err != nil { return err @@ -64,9 +63,7 @@ func (c *connWriter) WriteMsg(when time.Time, msg string, level int) error { defer c.innerWriter.Close() } - msg = formatLogTime(when) + msg - - c.lg.Println(msg) + c.lg.println(when, msg) return nil } @@ -98,11 +95,11 @@ func (c *connWriter) connect() error { } c.innerWriter = conn - c.lg = log.New(conn, "", 0) + c.lg = newLogWriter(conn) return nil } -func (c *connWriter) neddedConnectOnMsg() bool { +func (c *connWriter) needToConnectOnMsg() bool { if c.Reconnect { c.Reconnect = false return true diff --git a/logs/console.go b/logs/console.go index 3781e6cd..dc41dd7d 100644 --- a/logs/console.go +++ b/logs/console.go @@ -16,7 +16,6 @@ package logs import ( "encoding/json" - "log" "os" "runtime" "time" @@ -47,7 +46,7 @@ var colors = []brush{ // consoleWriter implements LoggerInterface and writes messages to terminal. type consoleWriter struct { - lg *log.Logger + lg *logWriter Level int `json:"level"` Colorful bool `json:"color"` //this filed is useful only when system's terminal supports color } @@ -55,9 +54,9 @@ type consoleWriter struct { // NewConsole create ConsoleWriter returning as LoggerInterface. func NewConsole() Logger { cw := &consoleWriter{ - lg: log.New(os.Stdout, "", 0), + lg: newLogWriter(os.Stdout), Level: LevelDebug, - Colorful: true, + Colorful: runtime.GOOS != "windows", } return cw } @@ -80,12 +79,10 @@ func (c *consoleWriter) WriteMsg(when time.Time, msg string, level int) error { if level > c.Level { return nil } - msg = formatLogTime(when) + msg if c.Colorful { - c.lg.Println(colors[level](msg)) - } else { - c.lg.Println(msg) + msg = colors[level](msg) } + c.lg.println(when, msg) return nil } diff --git a/logs/file.go b/logs/file.go index 3a042164..9d3f78a0 100644 --- a/logs/file.go +++ b/logs/file.go @@ -53,9 +53,11 @@ type fileLogWriter struct { Level int `json:"level"` Perm os.FileMode `json:"perm"` + + fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix } -// NewFileWriter create a FileLogWriter returning as LoggerInterface. +// newFileWriter create a FileLogWriter returning as LoggerInterface. func newFileWriter() Logger { w := &fileLogWriter{ Filename: "", @@ -89,6 +91,11 @@ func (w *fileLogWriter) Init(jsonConfig string) error { if len(w.Filename) == 0 { return errors.New("jsonconfig must have filename") } + w.suffix = filepath.Ext(w.Filename) + w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix) + if w.suffix == "" { + w.suffix = ".log" + } err = w.startLogger() return err } @@ -118,10 +125,9 @@ func (w *fileLogWriter) WriteMsg(when time.Time, msg string, level int) error { if level > w.Level { return nil } - msg = formatLogTime(when) + msg + "\n" - + h, d := formatTimeHeader(when) + msg = string(h) + msg + "\n" if w.Rotate { - d := when.Day() if w.needRotate(len(msg), d) { w.Lock() if w.needRotate(len(msg), d) { @@ -196,7 +202,7 @@ func (w *fileLogWriter) lines() (int, error) { } // DoRotate means it need to write file in new file. -// new file name like xx.2013-01-01.2.log +// new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size) func (w *fileLogWriter) doRotate(logTime time.Time) error { _, err := os.Lstat(w.Filename) if err != nil { @@ -206,13 +212,13 @@ func (w *fileLogWriter) doRotate(logTime time.Time) error { // Find the next available number num := 1 fName := "" - suffix := filepath.Ext(w.Filename) - filenameOnly := strings.TrimSuffix(w.Filename, suffix) - if suffix == "" { - suffix = ".log" - } - for ; err == nil && num <= 999; num++ { - fName = filenameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, suffix) + if w.MaxLines > 0 || w.MaxSize > 0 { + for ; err == nil && num <= 999; num++ { + fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, w.suffix) + _, err = os.Lstat(fName) + } + } else { + fName = fmt.Sprintf("%s.%s%s", w.fileNameOnly, logTime.Format("2006-01-02"), w.suffix) _, err = os.Lstat(fName) } // return error if the last file checked still existed @@ -250,7 +256,8 @@ func (w *fileLogWriter) deleteOldLog() { }() if !info.IsDir() && info.ModTime().Unix() < (time.Now().Unix()-60*60*24*w.MaxDays) { - if strings.HasPrefix(filepath.Base(path), filepath.Base(w.Filename)) { + if strings.HasPrefix(filepath.Base(path), w.fileNameOnly) && + strings.HasSuffix(filepath.Base(path), w.suffix) { os.Remove(path) } } diff --git a/logs/log.go b/logs/log.go index 428e8e50..a5982e45 100644 --- a/logs/log.go +++ b/logs/log.go @@ -146,17 +146,25 @@ func (bl *BeeLogger) Async() *BeeLogger { func (bl *BeeLogger) SetLogger(adapterName string, config string) error { bl.lock.Lock() defer bl.lock.Unlock() - if log, ok := adapters[adapterName]; ok { - lg := log() - err := lg.Init(config) - if err != nil { - fmt.Fprintln(os.Stderr, "logs.BeeLogger.SetLogger: "+err.Error()) - return err + + for _, l := range bl.outputs { + if l.name == adapterName { + return fmt.Errorf("logs: duplicate adaptername %q (you have set this logger before)", adapterName) } - bl.outputs = append(bl.outputs, &nameLogger{name: adapterName, Logger: lg}) - } else { + } + + log, ok := adapters[adapterName] + if !ok { return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName) } + + lg := log() + err := lg.Init(config) + if err != nil { + fmt.Fprintln(os.Stderr, "logs.BeeLogger.SetLogger: "+err.Error()) + return err + } + bl.outputs = append(bl.outputs, &nameLogger{name: adapterName, Logger: lg}) return nil } @@ -412,45 +420,3 @@ func (bl *BeeLogger) flush() { l.Flush() } } - -func formatLogTime(when time.Time) string { - y, mo, d := when.Date() - h, mi, s := when.Clock() - //len(2006/01/02 15:03:04)==19 - var buf [20]byte - t := 3 - for y >= 10 { - p := y / 10 - buf[t] = byte('0' + y - p*10) - y = p - t-- - } - buf[0] = byte('0' + y) - buf[4] = '/' - if mo > 9 { - buf[5] = '1' - buf[6] = byte('0' + mo - 9) - } else { - buf[5] = '0' - buf[6] = byte('0' + mo) - } - buf[7] = '/' - t = d / 10 - buf[8] = byte('0' + t) - buf[9] = byte('0' + d - t*10) - buf[10] = ' ' - t = h / 10 - buf[11] = byte('0' + t) - buf[12] = byte('0' + h - t*10) - buf[13] = ':' - t = mi / 10 - buf[14] = byte('0' + t) - buf[15] = byte('0' + mi - t*10) - buf[16] = ':' - t = s / 10 - buf[17] = byte('0' + t) - buf[18] = byte('0' + s - t*10) - buf[19] = ' ' - - return string(buf[0:]) -} diff --git a/logs/logger.go b/logs/logger.go new file mode 100644 index 00000000..b25bfaef --- /dev/null +++ b/logs/logger.go @@ -0,0 +1,79 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "io" + "sync" + "time" +) + +type logWriter struct { + sync.Mutex + writer io.Writer +} + +func newLogWriter(wr io.Writer) *logWriter { + return &logWriter{writer: wr} +} + +func (lg *logWriter) println(when time.Time, msg string) { + lg.Lock() + h, _ := formatTimeHeader(when) + lg.writer.Write(append(append(h, msg...), '\n')) + lg.Unlock() +} + +func formatTimeHeader(when time.Time) ([]byte, int) { + y, mo, d := when.Date() + h, mi, s := when.Clock() + //len(2006/01/02 15:03:04)==19 + var buf [20]byte + t := 3 + for y >= 10 { + p := y / 10 + buf[t] = byte('0' + y - p*10) + y = p + t-- + } + buf[0] = byte('0' + y) + buf[4] = '/' + if mo > 9 { + buf[5] = '1' + buf[6] = byte('0' + mo - 9) + } else { + buf[5] = '0' + buf[6] = byte('0' + mo) + } + buf[7] = '/' + t = d / 10 + buf[8] = byte('0' + t) + buf[9] = byte('0' + d - t*10) + buf[10] = ' ' + t = h / 10 + buf[11] = byte('0' + t) + buf[12] = byte('0' + h - t*10) + buf[13] = ':' + t = mi / 10 + buf[14] = byte('0' + t) + buf[15] = byte('0' + mi - t*10) + buf[16] = ':' + t = s / 10 + buf[17] = byte('0' + t) + buf[18] = byte('0' + s - t*10) + buf[19] = ' ' + + return buf[0:], d +} diff --git a/logs/multifile.go b/logs/multifile.go new file mode 100644 index 00000000..b82ba274 --- /dev/null +++ b/logs/multifile.go @@ -0,0 +1,116 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "encoding/json" + "time" +) + +// A filesLogWriter manages several fileLogWriter +// filesLogWriter will write logs to the file in json configuration and write the same level log to correspond file +// means if the file name in configuration is project.log filesLogWriter will create project.error.log/project.debug.log +// and write the error-level logs to project.error.log and write the debug-level logs to project.debug.log +// the rotate attribute also acts like fileLogWriter +type multiFileLogWriter struct { + writers [LevelDebug + 1 + 1]*fileLogWriter // the last one for fullLogWriter + fullLogWriter *fileLogWriter + Separate []string `json:"separate"` +} + +var levelNames = [...]string{"emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"} + +// Init file logger with json config. +// jsonConfig like: +// { +// "filename":"logs/beego.log", +// "maxLines":0, +// "maxsize":0, +// "daily":true, +// "maxDays":15, +// "rotate":true, +// "perm":0600, +// "separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"], +// } + +func (f *multiFileLogWriter) Init(config string) error { + writer := newFileWriter().(*fileLogWriter) + err := writer.Init(config) + if err != nil { + return err + } + f.fullLogWriter = writer + f.writers[LevelDebug+1] = writer + + //unmarshal "separate" field to f.Separate + json.Unmarshal([]byte(config), f) + + jsonMap := map[string]interface{}{} + json.Unmarshal([]byte(config), &jsonMap) + + for i := LevelEmergency; i < LevelDebug+1; i++ { + for _, v := range f.Separate { + if v == levelNames[i] { + jsonMap["filename"] = f.fullLogWriter.fileNameOnly + "." + levelNames[i] + f.fullLogWriter.suffix + jsonMap["level"] = i + bs, _ := json.Marshal(jsonMap) + writer = newFileWriter().(*fileLogWriter) + writer.Init(string(bs)) + f.writers[i] = writer + } + } + } + + return nil +} + +func (f *multiFileLogWriter) Destroy() { + for i := 0; i < len(f.writers); i++ { + if f.writers[i] != nil { + f.writers[i].Destroy() + } + } +} + +func (f *multiFileLogWriter) WriteMsg(when time.Time, msg string, level int) error { + if f.fullLogWriter != nil { + f.fullLogWriter.WriteMsg(when, msg, level) + } + for i := 0; i < len(f.writers)-1; i++ { + if f.writers[i] != nil { + if level == f.writers[i].Level { + f.writers[i].WriteMsg(when, msg, level) + } + } + } + return nil +} + +func (f *multiFileLogWriter) Flush() { + for i := 0; i < len(f.writers); i++ { + if f.writers[i] != nil { + f.writers[i].Flush() + } + } +} + +// newFilesWriter create a FileLogWriter returning as LoggerInterface. +func newFilesWriter() Logger { + return &multiFileLogWriter{} +} + +func init() { + Register("multifile", newFilesWriter) +} diff --git a/logs/multifile_test.go b/logs/multifile_test.go new file mode 100644 index 00000000..57b96094 --- /dev/null +++ b/logs/multifile_test.go @@ -0,0 +1,78 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "bufio" + "os" + "strconv" + "strings" + "testing" +) + +func TestFiles_1(t *testing.T) { + log := NewLogger(10000) + log.SetLogger("multifile", `{"filename":"test.log","separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"]}`) + log.Debug("debug") + log.Informational("info") + log.Notice("notice") + log.Warning("warning") + log.Error("error") + log.Alert("alert") + log.Critical("critical") + log.Emergency("emergency") + fns := []string{""} + fns = append(fns, levelNames[0:]...) + name := "test" + suffix := ".log" + for _, fn := range fns { + + file := name + suffix + if fn != "" { + file = name + "." + fn + suffix + } + f, err := os.Open(file) + if err != nil { + t.Fatal(err) + } + b := bufio.NewReader(f) + lineNum := 0 + lastLine := "" + for { + line, _, err := b.ReadLine() + if err != nil { + break + } + if len(line) > 0 { + lastLine = string(line) + lineNum++ + } + } + var expected = 1 + if fn == "" { + expected = LevelDebug + 1 + } + if lineNum != expected { + t.Fatal(file, "has", lineNum, "lines not "+strconv.Itoa(expected)+" lines") + } + if lineNum == 1 { + if !strings.Contains(lastLine, fn) { + t.Fatal(file + " " + lastLine + " not contains the log msg " + fn) + } + } + os.Remove(file) + } + +} diff --git a/namespace.go b/namespace.go index 0dfdd7af..4007d44c 100644 --- a/namespace.go +++ b/namespace.go @@ -388,3 +388,10 @@ func NSNamespace(prefix string, params ...LinkNamespace) LinkNamespace { ns.Namespace(n) } } + +// NSHandler add handler +func NSHandler(rootpath string, h http.Handler) LinkNamespace { + return func(ns *Namespace) { + ns.Handler(rootpath, h) + } +} diff --git a/orm/db.go b/orm/db.go index b62c165b..314c3535 100644 --- a/orm/db.go +++ b/orm/db.go @@ -113,7 +113,7 @@ func (d *dbBase) collectFieldValue(mi *modelInfo, fi *fieldInfo, ind reflect.Val if fi.pk { _, value, _ = getExistPk(mi, ind) } else { - field := ind.Field(fi.fieldIndex) + field := ind.FieldByIndex(fi.fieldIndex) if fi.isFielder { f := field.Addr().Interface().(Fielder) value = f.RawValue() @@ -517,9 +517,9 @@ func (d *dbBase) Delete(q dbQuerier, mi *modelInfo, ind reflect.Value, tz *time. if num > 0 { if mi.fields.pk.auto { if mi.fields.pk.fieldType&IsPostiveIntegerField > 0 { - ind.Field(mi.fields.pk.fieldIndex).SetUint(0) + ind.FieldByIndex(mi.fields.pk.fieldIndex).SetUint(0) } else { - ind.Field(mi.fields.pk.fieldIndex).SetInt(0) + ind.FieldByIndex(mi.fields.pk.fieldIndex).SetInt(0) } } err := d.deleteRels(q, mi, []interface{}{pkValue}, tz) @@ -859,13 +859,13 @@ func (d *dbBase) ReadBatch(q dbQuerier, qs *querySet, mi *modelInfo, cond *Condi mmi = fi.relModelInfo field := last if last.Kind() != reflect.Invalid { - field = reflect.Indirect(last.Field(fi.fieldIndex)) + field = reflect.Indirect(last.FieldByIndex(fi.fieldIndex)) if field.IsValid() { d.setColsValues(mmi, &field, mmi.fields.dbcols, trefs[:len(mmi.fields.dbcols)], tz) for _, fi := range mmi.fields.fieldsReverse { if fi.inModel && fi.reverseFieldInfo.mi == lastm { if fi.reverseFieldInfo != nil { - f := field.Field(fi.fieldIndex) + f := field.FieldByIndex(fi.fieldIndex) if f.Kind() == reflect.Ptr { f.Set(last.Addr()) } @@ -1014,7 +1014,7 @@ func (d *dbBase) setColsValues(mi *modelInfo, ind *reflect.Value, cols []string, fi := mi.fields.GetByColumn(column) - field := ind.Field(fi.fieldIndex) + field := ind.FieldByIndex(fi.fieldIndex) value, err := d.convertValueFromDB(fi, val, tz) if err != nil { @@ -1350,7 +1350,7 @@ setValue: fieldType = fi.relModelInfo.fields.pk.fieldType mf := reflect.New(fi.relModelInfo.addrField.Elem().Type()) field.Set(mf) - f := mf.Elem().Field(fi.relModelInfo.fields.pk.fieldIndex) + f := mf.Elem().FieldByIndex(fi.relModelInfo.fields.pk.fieldIndex) field = f goto setValue } diff --git a/orm/db_alias.go b/orm/db_alias.go index 79576b8e..b6c833a7 100644 --- a/orm/db_alias.go +++ b/orm/db_alias.go @@ -59,6 +59,7 @@ var ( "postgres": DRPostgres, "sqlite3": DRSqlite, "tidb": DRTiDB, + "oracle": DROracle, } dbBasers = map[DriverType]dbBaser{ DRMySQL: newdbBaseMysql(), @@ -151,7 +152,7 @@ func detectTZ(al *alias) { al.Engine = "INNODB" } - case DRSqlite: + case DRSqlite, DROracle: al.TZ = time.UTC case DRPostgres: diff --git a/orm/db_oracle.go b/orm/db_oracle.go index 1e385c9a..deca36ad 100644 --- a/orm/db_oracle.go +++ b/orm/db_oracle.go @@ -14,6 +14,41 @@ package orm +import ( + "fmt" + "strings" +) + +// oracle operators. +var oracleOperators = map[string]string{ + "exact": "= ?", + "gt": "> ?", + "gte": ">= ?", + "lt": "< ?", + "lte": "<= ?", + "//iendswith": "LIKE ?", +} + +// oracle column field types. +var oracleTypes = map[string]string{ + "pk": "NOT NULL PRIMARY KEY", + "bool": "bool", + "string": "VARCHAR2(%d)", + "string-text": "VARCHAR2(%d)", + "time.Time-date": "DATE", + "time.Time": "TIMESTAMP", + "int8": "INTEGER", + "int16": "INTEGER", + "int32": "INTEGER", + "int64": "INTEGER", + "uint8": "INTEGER", + "uint16": "INTEGER", + "uint32": "INTEGER", + "uint64": "INTEGER", + "float64": "NUMBER", + "float64-decimal": "NUMBER(%d, %d)", +} + // oracle dbBaser type dbBaseOracle struct { dbBase @@ -27,3 +62,35 @@ func newdbBaseOracle() dbBaser { b.ins = b return b } + +// OperatorSQL get oracle operator. +func (d *dbBaseOracle) OperatorSQL(operator string) string { + return oracleOperators[operator] +} + +// DbTypes get oracle table field types. +func (d *dbBaseOracle) DbTypes() map[string]string { + return oracleTypes +} + +//ShowTablesQuery show all the tables in database +func (d *dbBaseOracle) ShowTablesQuery() string { + return "SELECT TABLE_NAME FROM USER_TABLES" +} + +// Oracle +func (d *dbBaseOracle) ShowColumnsQuery(table string) string { + return fmt.Sprintf("SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS "+ + "WHERE TABLE_NAME ='%s'", strings.ToUpper(table)) +} + +// check index is exist +func (d *dbBaseOracle) IndexExists(db dbQuerier, table string, name string) bool { + row := db.QueryRow("SELECT COUNT(*) FROM USER_IND_COLUMNS, USER_INDEXES "+ + "WHERE USER_IND_COLUMNS.INDEX_NAME = USER_INDEXES.INDEX_NAME "+ + "AND USER_IND_COLUMNS.TABLE_NAME = ? AND USER_IND_COLUMNS.INDEX_NAME = ?", strings.ToUpper(table), strings.ToUpper(name)) + + var cnt int + row.Scan(&cnt) + return cnt > 0 +} diff --git a/orm/db_utils.go b/orm/db_utils.go index ae9b1625..c97caf36 100644 --- a/orm/db_utils.go +++ b/orm/db_utils.go @@ -32,7 +32,7 @@ func getDbAlias(name string) *alias { func getExistPk(mi *modelInfo, ind reflect.Value) (column string, value interface{}, exist bool) { fi := mi.fields.pk - v := ind.Field(fi.fieldIndex) + v := ind.FieldByIndex(fi.fieldIndex) if fi.fieldType&IsPostiveIntegerField > 0 { vu := v.Uint() exist = vu > 0 diff --git a/orm/models_info_f.go b/orm/models_info_f.go index 14e1f2c6..996a2f40 100644 --- a/orm/models_info_f.go +++ b/orm/models_info_f.go @@ -102,7 +102,7 @@ func newFields() *fields { // single field info type fieldInfo struct { mi *modelInfo - fieldIndex int + fieldIndex []int fieldType int dbcol bool inModel bool @@ -138,7 +138,7 @@ type fieldInfo struct { } // new field info -func newFieldInfo(mi *modelInfo, field reflect.Value, sf reflect.StructField) (fi *fieldInfo, err error) { +func newFieldInfo(mi *modelInfo, field reflect.Value, sf reflect.StructField, mName string) (fi *fieldInfo, err error) { var ( tag string tagValue string @@ -278,7 +278,7 @@ checkType: fi.column = getColumnName(fieldType, addrField, sf, tags["column"]) fi.addrValue = addrField fi.sf = sf - fi.fullName = mi.fullName + "." + sf.Name + fi.fullName = mi.fullName + mName + "." + sf.Name fi.null = attrs["null"] fi.index = attrs["index"] diff --git a/orm/models_info_m.go b/orm/models_info_m.go index 2654cdb5..bbb82444 100644 --- a/orm/models_info_m.go +++ b/orm/models_info_m.go @@ -36,11 +36,6 @@ type modelInfo struct { // new model info func newModelInfo(val reflect.Value) (info *modelInfo) { - var ( - err error - fi *fieldInfo - sf reflect.StructField - ) info = &modelInfo{} info.fields = newFields() @@ -53,13 +48,31 @@ func newModelInfo(val reflect.Value) (info *modelInfo) { info.name = typ.Name() info.fullName = getFullName(typ) + addModelFields(info, ind, "", []int{}) + + return +} + +func addModelFields(info *modelInfo, ind reflect.Value, mName string, index []int) { + var ( + err error + fi *fieldInfo + sf reflect.StructField + ) + for i := 0; i < ind.NumField(); i++ { field := ind.Field(i) sf = ind.Type().Field(i) if sf.PkgPath != "" { continue } - fi, err = newFieldInfo(info, field, sf) + // add anonymous struct fields + if sf.Anonymous { + addModelFields(info, field, mName+"."+sf.Name, append(index, i)) + continue + } + + fi, err = newFieldInfo(info, field, sf, mName) if err != nil { if err == errSkipField { @@ -84,7 +97,7 @@ func newModelInfo(val reflect.Value) (info *modelInfo) { } } - fi.fieldIndex = i + fi.fieldIndex = append(index, i) fi.mi = info fi.inModel = true } @@ -93,8 +106,6 @@ func newModelInfo(val reflect.Value) (info *modelInfo) { fmt.Println(fmt.Errorf("field: %s.%s, %s", ind.Type(), sf.Name, err)) os.Exit(2) } - - return } // combine related model info to new model info. diff --git a/orm/models_test.go b/orm/models_test.go index ee56e8e8..ffb16ea0 100644 --- a/orm/models_test.go +++ b/orm/models_test.go @@ -25,7 +25,6 @@ import ( _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" - // As tidb can't use go get, so disable the tidb testing now // _ "github.com/pingcap/tidb" ) @@ -352,6 +351,30 @@ type GroupPermissions struct { Permission *Permission `orm:"rel(fk)"` } +type ModelID struct { + ID int64 +} + +type ModelBase struct { + ModelID + + Created time.Time `orm:"auto_now_add;type(datetime)"` + Updated time.Time `orm:"auto_now;type(datetime)"` +} + +type InLine struct { + // Common Fields + ModelBase + + // Other Fields + Name string `orm:"unique"` + Email string +} + +func NewInLine() *InLine { + return new(InLine) +} + var DBARGS = struct { Driver string Source string diff --git a/orm/orm.go b/orm/orm.go index d00d6d03..0ffb6b86 100644 --- a/orm/orm.go +++ b/orm/orm.go @@ -140,7 +140,7 @@ func (o *orm) ReadOrCreate(md interface{}, col1 string, cols ...string) (bool, i return (err == nil), id, err } - return false, ind.Field(mi.fields.pk.fieldIndex).Int(), err + return false, ind.FieldByIndex(mi.fields.pk.fieldIndex).Int(), err } // insert model data to database @@ -160,9 +160,9 @@ func (o *orm) Insert(md interface{}) (int64, error) { func (o *orm) setPk(mi *modelInfo, ind reflect.Value, id int64) { if mi.fields.pk.auto { if mi.fields.pk.fieldType&IsPostiveIntegerField > 0 { - ind.Field(mi.fields.pk.fieldIndex).SetUint(uint64(id)) + ind.FieldByIndex(mi.fields.pk.fieldIndex).SetUint(uint64(id)) } else { - ind.Field(mi.fields.pk.fieldIndex).SetInt(id) + ind.FieldByIndex(mi.fields.pk.fieldIndex).SetInt(id) } } } @@ -290,7 +290,7 @@ func (o *orm) LoadRelated(md interface{}, name string, args ...interface{}) (int qs.orders = []string{order} } - find := ind.Field(fi.fieldIndex) + find := ind.FieldByIndex(fi.fieldIndex) var nums int64 var err error diff --git a/orm/orm_object.go b/orm/orm_object.go index df5a6600..8a5d85e2 100644 --- a/orm/orm_object.go +++ b/orm/orm_object.go @@ -51,9 +51,9 @@ func (o *insertSet) Insert(md interface{}) (int64, error) { if id > 0 { if o.mi.fields.pk.auto { if o.mi.fields.pk.fieldType&IsPostiveIntegerField > 0 { - ind.Field(o.mi.fields.pk.fieldIndex).SetUint(uint64(id)) + ind.FieldByIndex(o.mi.fields.pk.fieldIndex).SetUint(uint64(id)) } else { - ind.Field(o.mi.fields.pk.fieldIndex).SetInt(id) + ind.FieldByIndex(o.mi.fields.pk.fieldIndex).SetInt(id) } } } diff --git a/orm/orm_raw.go b/orm/orm_raw.go index cbb18064..5f88121c 100644 --- a/orm/orm_raw.go +++ b/orm/orm_raw.go @@ -342,7 +342,7 @@ func (o *rawSet) QueryRow(containers ...interface{}) error { for _, col := range columns { if fi := sMi.fields.GetByColumn(col); fi != nil { value := reflect.ValueOf(columnsMp[col]).Elem().Interface() - o.setFieldValue(ind.FieldByIndex([]int{fi.fieldIndex}), value) + o.setFieldValue(ind.FieldByIndex(fi.fieldIndex), value) } } } else { @@ -480,7 +480,7 @@ func (o *rawSet) QueryRows(containers ...interface{}) (int64, error) { for _, col := range columns { if fi := sMi.fields.GetByColumn(col); fi != nil { value := reflect.ValueOf(columnsMp[col]).Elem().Interface() - o.setFieldValue(ind.FieldByIndex([]int{fi.fieldIndex}), value) + o.setFieldValue(ind.FieldByIndex(fi.fieldIndex), value) } } } else { diff --git a/orm/orm_test.go b/orm/orm_test.go index d6f6c7a9..ec0f0d3a 100644 --- a/orm/orm_test.go +++ b/orm/orm_test.go @@ -187,6 +187,7 @@ func TestSyncDb(t *testing.T) { RegisterModel(new(Group)) RegisterModel(new(Permission)) RegisterModel(new(GroupPermissions)) + RegisterModel(new(InLine)) err := RunSyncdb("default", true, Debug) throwFail(t, err) @@ -206,6 +207,7 @@ func TestRegisterModels(t *testing.T) { RegisterModel(new(Group)) RegisterModel(new(Permission)) RegisterModel(new(GroupPermissions)) + RegisterModel(new(InLine)) BootStrap() @@ -1928,3 +1930,25 @@ func TestReadOrCreate(t *testing.T) { dORM.Delete(u) } + +func TestInLine(t *testing.T) { + name := "inline" + email := "hello@go.com" + inline := NewInLine() + inline.Name = name + inline.Email = email + + id, err := dORM.Insert(inline) + throwFail(t, err) + throwFail(t, AssertIs(id, 1)) + + il := NewInLine() + il.ID = 1 + err = dORM.Read(il) + throwFail(t, err) + + throwFail(t, AssertIs(il.Name, name)) + throwFail(t, AssertIs(il.Email, email)) + throwFail(t, AssertIs(il.Created.In(DefaultTimeLoc), inline.Created.In(DefaultTimeLoc), testDate)) + throwFail(t, AssertIs(il.Updated.In(DefaultTimeLoc), inline.Updated.In(DefaultTimeLoc), testDateTime)) +} diff --git a/parser.go b/parser.go index f23f4720..46d02320 100644 --- a/parser.go +++ b/parser.go @@ -58,7 +58,7 @@ func parserPkg(pkgRealpath, pkgpath string) error { rep := strings.NewReplacer("/", "_", ".", "_") commentFilename = coomentPrefix + rep.Replace(pkgpath) + ".go" if !compareFile(pkgRealpath) { - Info(pkgRealpath + " has not changed, not reloading") + Info(pkgRealpath + " no changed") return nil } genInfoList = make(map[string][]ControllerComments) diff --git a/plugins/apiauth/apiauth.go b/plugins/apiauth/apiauth.go index 8af08088..970367c9 100644 --- a/plugins/apiauth/apiauth.go +++ b/plugins/apiauth/apiauth.go @@ -35,7 +35,7 @@ // // beego.InsertFilter("*", beego.BeforeRouter,apiauth.APISecretAuth(getAppSecret, 360)) // -// Infomation: +// Information: // // In the request user should include these params in the query // diff --git a/router.go b/router.go index 82a602e1..d0bf534f 100644 --- a/router.go +++ b/router.go @@ -607,6 +607,7 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) ) context := p.pool.Get().(*beecontext.Context) context.Reset(rw, r) + defer p.pool.Put(context) defer p.recoverPanic(context) diff --git a/router_test.go b/router_test.go index b0ae7a18..f26f0c86 100644 --- a/router_test.go +++ b/router_test.go @@ -65,6 +65,11 @@ func (tc *TestController) GetManyRouter() { tc.Ctx.WriteString(tc.Ctx.Input.Query(":id") + tc.Ctx.Input.Query(":page")) } +func (tc *TestController) GetEmptyBody() { + var res []byte + tc.Ctx.Output.Body(res) +} + type ResStatus struct { Code int Msg string @@ -239,6 +244,21 @@ func TestManyRoute(t *testing.T) { } } +// Test for issue #1669 +func TestEmptyResponse(t *testing.T) { + + r, _ := http.NewRequest("GET", "/beego-empty.html", nil) + w := httptest.NewRecorder() + + handler := NewControllerRegister() + handler.Add("/beego-empty.html", &TestController{}, "get:GetEmptyBody") + handler.ServeHTTP(w, r) + + if body := w.Body.String(); body != "" { + t.Error("want empty body") + } +} + func TestNotFound(t *testing.T) { r, _ := http.NewRequest("GET", "/", nil) w := httptest.NewRecorder() diff --git a/staticfile.go b/staticfile.go index 9534ce91..0aad2c81 100644 --- a/staticfile.go +++ b/staticfile.go @@ -93,12 +93,14 @@ type serveContentHolder struct { var ( staticFileMap = make(map[string]*serveContentHolder) - mapLock sync.Mutex + mapLock sync.RWMutex ) func openFile(filePath string, fi os.FileInfo, acceptEncoding string) (bool, string, *serveContentHolder, error) { mapKey := acceptEncoding + ":" + filePath + mapLock.RLock() mapFile, _ := staticFileMap[mapKey] + mapLock.RUnlock() if isOk(mapFile, fi) { return mapFile.encoding != "", mapFile.encoding, mapFile, nil } diff --git a/staticfile_test.go b/staticfile_test.go index e7003366..a043b4fd 100644 --- a/staticfile_test.go +++ b/staticfile_test.go @@ -7,8 +7,8 @@ import ( "io" "io/ioutil" "os" - "testing" "path/filepath" + "testing" ) var currentWorkDir, _ = os.Getwd() diff --git a/template.go b/template.go index 9954cf32..e6c43f87 100644 --- a/template.go +++ b/template.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "html/template" + "io" "io/ioutil" "os" "path/filepath" @@ -30,13 +31,28 @@ import ( var ( beegoTplFuncMap = make(template.FuncMap) - // BeeTemplates caching map and supported template file extensions. - BeeTemplates = make(map[string]*template.Template) - templatesLock sync.Mutex - // BeeTemplateExt stores the template extension which will build - BeeTemplateExt = []string{"tpl", "html"} + // beeTemplates caching map and supported template file extensions. + beeTemplates = make(map[string]*template.Template) + templatesLock sync.RWMutex + // beeTemplateExt stores the template extension which will build + beeTemplateExt = []string{"tpl", "html"} ) +func executeTemplate(wr io.Writer, name string, data interface{}) error { + if BConfig.RunMode == DEV { + templatesLock.RLock() + defer templatesLock.RUnlock() + } + if t, ok := beeTemplates[name]; ok { + err := t.ExecuteTemplate(wr, name, data) + if err != nil { + Trace("template Execute err:", err) + } + return err + } + panic("can't find templatefile in the path:" + name) +} + func init() { beegoTplFuncMap["dateformat"] = DateFormat beegoTplFuncMap["date"] = Date @@ -55,7 +71,6 @@ func init() { beegoTplFuncMap["config"] = GetConfig beegoTplFuncMap["map_get"] = MapGet - // go1.2 added template funcs // Comparisons beegoTplFuncMap["eq"] = eq // == beegoTplFuncMap["ge"] = ge // >= @@ -103,7 +118,7 @@ func (tf *templateFile) visit(paths string, f os.FileInfo, err error) error { // HasTemplateExt return this path contains supported template extension of beego or not. func HasTemplateExt(paths string) bool { - for _, v := range BeeTemplateExt { + for _, v := range beeTemplateExt { if strings.HasSuffix(paths, "."+v) { return true } @@ -113,12 +128,12 @@ func HasTemplateExt(paths string) bool { // AddTemplateExt add new extension for template. func AddTemplateExt(ext string) { - for _, v := range BeeTemplateExt { + for _, v := range beeTemplateExt { if v == ext { return } } - BeeTemplateExt = append(BeeTemplateExt, ext) + beeTemplateExt = append(beeTemplateExt, ext) } // BuildTemplate will build all template files in a directory. @@ -149,7 +164,7 @@ func BuildTemplate(dir string, files ...string) error { if err != nil { Trace("parse template err:", file, err) } else { - BeeTemplates[file] = t + beeTemplates[file] = t } templatesLock.Unlock() } diff --git a/template_test.go b/template_test.go index 2e222efc..4f13736c 100644 --- a/template_test.go +++ b/template_test.go @@ -70,10 +70,10 @@ func TestTemplate(t *testing.T) { if err := BuildTemplate(dir); err != nil { t.Fatal(err) } - if len(BeeTemplates) != 3 { - t.Fatalf("should be 3 but got %v", len(BeeTemplates)) + if len(beeTemplates) != 3 { + t.Fatalf("should be 3 but got %v", len(beeTemplates)) } - if err := BeeTemplates["index.tpl"].ExecuteTemplate(os.Stdout, "index.tpl", nil); err != nil { + if err := beeTemplates["index.tpl"].ExecuteTemplate(os.Stdout, "index.tpl", nil); err != nil { t.Fatal(err) } for _, name := range files { @@ -126,7 +126,7 @@ func TestRelativeTemplate(t *testing.T) { if err := BuildTemplate(dir, files[1]); err != nil { t.Fatal(err) } - if err := BeeTemplates["easyui/rbac/user.tpl"].ExecuteTemplate(os.Stdout, "easyui/rbac/user.tpl", nil); err != nil { + if err := beeTemplates["easyui/rbac/user.tpl"].ExecuteTemplate(os.Stdout, "easyui/rbac/user.tpl", nil); err != nil { t.Fatal(err) } for _, name := range files { diff --git a/tree.go b/tree.go index 594e9999..25b78e50 100644 --- a/tree.go +++ b/tree.go @@ -141,7 +141,7 @@ func (t *Tree) addtree(segments []string, tree *Tree, wildcards []string, reg st regexpStr = "([^.]+).(.+)" params = params[1:] } else { - for _ = range params { + for range params { regexpStr = "([^/]+)/" + regexpStr } } @@ -254,7 +254,7 @@ func (t *Tree) addseg(segments []string, route interface{}, wildcards []string, regexpStr = "/([^.]+).(.+)" params = params[1:] } else { - for _ = range params { + for range params { regexpStr = "/([^/]+)" + regexpStr } } @@ -389,7 +389,7 @@ type leafInfo struct { func (leaf *leafInfo) match(wildcardValues []string, ctx *context.Context) (ok bool) { //fmt.Println("Leaf:", wildcardValues, leaf.wildcards, leaf.regexps) if leaf.regexps == nil { - if len(wildcardValues) == 0 { // static path + if len(wildcardValues) == 0 && len(leaf.wildcards) == 0 { // static path return true } // match * diff --git a/tree_test.go b/tree_test.go index 531df046..81ff7edd 100644 --- a/tree_test.go +++ b/tree_test.go @@ -97,6 +97,21 @@ func TestTreeRouters(t *testing.T) { } } +func TestStaticPath(t *testing.T) { + tr := NewTree() + tr.AddRouter("/topic/:id", "wildcard") + tr.AddRouter("/topic", "static") + ctx := context.NewContext() + obj := tr.Match("/topic", ctx) + if obj == nil || obj.(string) != "static" { + t.Fatal("/topic is a static route") + } + obj = tr.Match("/topic/1", ctx) + if obj == nil || obj.(string) != "wildcard" { + t.Fatal("/topic/1 is a wildcard route") + } +} + func TestAddTree(t *testing.T) { tr := NewTree() tr.AddRouter("/shop/:id/account", "astaxie") diff --git a/validation/validators.go b/validation/validators.go index ebff2191..9b04c5ce 100644 --- a/validation/validators.go +++ b/validation/validators.go @@ -588,7 +588,7 @@ func (b Base64) GetLimitValue() interface{} { } // just for chinese mobile phone number -var mobilePattern = regexp.MustCompile("^((\\+86)|(86))?(1(([35][0-9])|[8][0-9]|[7][0679]|[4][579]))\\d{8}$") +var mobilePattern = regexp.MustCompile("^((\\+86)|(86))?(1(([35][0-9])|[8][0-9]|[7][06789]|[4][579]))\\d{8}$") // Mobile check struct type Mobile struct {