Commits

Crane Jin committed af6f41f

Update to Go version 1.
Re-structure source files to follow the convension of Go

Comments (0)

Files changed (17)

 {
   "Server" : "127.0.0.1:27017",
     "DB" : "mysite",
-    "MapReduceInterval" : 43200,
-    "SessionTimeout" : 3600,
+    "MapReduceInterval" : "43200s",
+    "SessionTimeout" : "3600s",
     "PageSize" : 10,
     "MaxTagFont" : 36,
     "MinTagFont" : 10,
+package config
+
+import (
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"time"
+)
+
+var cfg map[string]interface{}
+var loaded = false
+
+func LoadConfig(path string) error {
+	b, err := ioutil.ReadFile(path)
+	if err != nil {
+		return err
+	}
+
+	var c interface{}
+	err = json.Unmarshal(b, &c)
+	if err != nil {
+		return err
+	}
+	cfg = c.(map[string]interface{})
+	loaded = true
+	return nil
+}
+
+func HasLoaded() bool {
+	return loaded
+}
+
+func GetString(key string) (string, error) {
+	v, ok := cfg[key]
+	if !ok {
+		return "", errors.New("No such key " + key)
+	}
+
+	return v.(string), nil
+}
+
+func GetDuration(key string) (time.Duration, error) {
+	var d time.Duration
+	var err error
+
+	v, ok := cfg[key]
+	if !ok {
+		return d, errors.New("No such key" + key)
+	}
+
+	switch t := v.(type) {
+	case string:
+		d,err = time.ParseDuration(t)
+	case float64:
+		d,err = time.Duration(t * 1e9), nil //seconds to nanoseconds
+	default:
+		err = errors.New("A duration value must be string or int64 (in seconds).")
+	}
+
+	return d, err
+}
+
+func GetInt64(key string) (int64, error) {
+	v, ok := cfg[key]
+	if !ok {
+		return -1, errors.New("No such key " + key)
+	}
+
+	return int64(v.(float64)), nil
+}
+
+func GetInt(key string) (int, error) {
+	v, ok := cfg[key]
+	if !ok {
+		return -1, errors.New("No such key " + key)
+	}
+
+	return int(v.(float64)), nil
+}
+
+func GetInt32(key string) (int32, error) {
+	v, ok := cfg[key]
+	if !ok {
+		return -1, errors.New("No such key " + key)
+	}
+
+	return int32(v.(float64)), nil
+}
+
+func GetFloat64(key string) (float64, error) {
+	v, ok := cfg[key]
+	if !ok {
+		return -1.0, errors.New("No such key " + key)
+	}
+
+	return v.(float64), nil
+}
+
+func GetFloat32(key string) (float32, error) {
+	v, ok := cfg[key]
+	if !ok {
+		return -1.0, errors.New("No such key " + key)
+	}
+
+	return float32(v.(float64)), nil
+}
+
+func GetBool(key string) (bool, error) {
+	v, ok := cfg[key]
+	if !ok {
+		return false, errors.New("No such key " + key)
+	}
+
+	return v.(bool), nil
+}
+
+func GetStringSlice(key string) ([]string, error) {
+	v, ok := cfg[key]
+	if !ok {
+		return nil, errors.New("No such key " + key)
+	}
+
+	temp := v.([]interface{})
+	result := make([]string, 0, len(temp))
+	for _, s := range temp {
+		result = append(result, s.(string))
+	}
+
+	return result, nil
+}

gdmk.go

-/* Built : Thu Dec  8 08:21:56 UTC 2011 */
-//-------------------------------------------------------------------
-// Auto generated code, but you are encouraged to modify it ☺
-// Manual: http://godag.googlecode.com
-//-------------------------------------------------------------------
-
-package main
-
-import (
-    "os"
-    "io"
-    "strings"
-    "compress/gzip"
-    "fmt"
-    "bytes"
-    "regexp"
-    "exec"
-    "log"
-    "flag"
-    "path/filepath"
-)
-
-
-//-------------------------------------------------------------------
-//  User defined targets: flexible pure go way to control build
-//-------------------------------------------------------------------
-
-type Target struct {
-    desc  string   // description of target
-    first func()   // this is called prior to building
-    last  func()   // this is called after building
-}
-
-/********************************************************************
-
- Code inside playground is NOT auto generated, each time you run
- this command:
-
-     gd -gdmk=somefilename.go
-
-  this happens:
-
-  if (somefilename.go already exists) {
-      transfer playground from somefilename.go to new version
-      of somefilename.go, i.e. you never loose the targets you
-      create inside the playground..
-  }else{
-      write somefilename.go with example targets
-  }
-
-********************************************************************/
-
-// PLAYGROUND START
-
-var targets = map[string]*Target{
-    "full": &Target{
-        desc:  "compile all packages (ignore !modified)",
-        first: func() { oldPkgFound = true },
-        last:  nil,
-    },
-    "install": &Target{
-        desc:  "install package in a typical Go fashion",
-        first: installDoFirst,
-        last:  nil,
-    },
-    "uninstall": &Target{
-        desc:  "remove object files from GOPATH||GOROOT",
-        first: func() {
-            installDoFirst() // same setup
-            clean = true
-        },
-        last:  nil,
-    },
-}
-
-func installDoFirst(){
-
-    var placement string
-
-    var env = map[string]string{
-        "GOOS"   : os.Getenv("GOOS"),
-        "GOARCH" : os.Getenv("GOARCH"),
-        "GOROOT" : os.Getenv("GOROOT"),
-        "GOPATH" : os.Getenv("GOPATH"),
-    }
-
-    if env["GOARCH"] == "" || env["GOOS"] == "" || env["GOROOT"] == "" {
-        log.Fatalf("GOARCH, GOROOT and GOOS variables must be set\n")
-    }
-
-    stub := env["GOOS"] + "_" + env["GOARCH"]
-
-    if env["GOPATH"] != "" {
-        placement = filepath.Join(env["GOPATH"], "pkg", stub)
-    }else{
-        placement = filepath.Join(env["GOROOT"], "pkg", stub)
-    }
-
-    includeDir = placement
-
-    for i := 0; i < len(packages); i++ {
-        packages[i].output = placement + packages[i].output[4:]
-    }
-}
-
-
-// PLAYGROUND STOP
-
-//-------------------------------------------------------------------
-// Execute user defined targets
-//-------------------------------------------------------------------
-
-func doFirst() {
-    args := flag.Args()
-    for i := 0; i < len(args); i++ {
-        target, ok := targets[args[i]]
-        if ok && target.first != nil {
-            target.first()
-        }
-    }
-}
-
-func doLast() {
-    args := flag.Args()
-    for i := 0; i < len(args); i++ {
-        target, ok := targets[args[i]]
-        if ok && target.last != nil {
-            target.last()
-        }
-    }
-}
-
-//-------------------------------------------------------------------
-// Simple way to turn print statements on/off
-//-------------------------------------------------------------------
-
-type Say bool
-
-func (s Say) Printf(frmt string, args ...interface{}) {
-    if bool(s) {
-        fmt.Printf(frmt, args...)
-    }
-}
-
-func (s Say) Println(args ...interface{}) {
-    if bool(s) {
-        fmt.Println(args...)
-    }
-}
-
-//-------------------------------------------------------------------
-// Initialize variables, flags and so on
-//-------------------------------------------------------------------
-
-var (
-    compiler    = ""
-    linker      = ""
-    suffix      = ""
-    backend     = ""
-    root        = ""
-    output      = ""
-    match       = ""
-    help        = false
-    list        = false
-    quiet       = false
-    external    = false
-    clean       = false
-    oldPkgFound = false
-)
-
-var includeDir = "_obj"
-
-var say = Say(true)
-
-
-func init() {
-
-    flag.StringVar(&backend, "backend", "gc", "select from [gc,gccgo,express]")
-    flag.StringVar(&backend, "B", "gc", "alias for --backend option")
-    flag.StringVar(&root, "I", "", "import package directory")
-    flag.StringVar(&match, "M", "", "regex to match main package")
-    flag.StringVar(&match, "main", "", "regex to match main package")
-    flag.StringVar(&output, "o", "", "link main package -> output")
-    flag.StringVar(&output, "output", "", "link main package -> output")
-    flag.BoolVar(&external, "external", false, "external dependencies")
-    flag.BoolVar(&external, "e", false, "external dependencies")
-    flag.BoolVar(&quiet, "q", false, "don't print anything but errors")
-    flag.BoolVar(&quiet, "quiet", false, "don't print anything but errors")
-    flag.BoolVar(&help, "h", false, "help message")
-    flag.BoolVar(&help, "help", false, "help message")
-    flag.BoolVar(&clean, "clean", false, "delete objects")
-    flag.BoolVar(&clean, "c", false, "delete objects")
-    flag.BoolVar(&list, "list", false, "list targets for bash autocomplete")
-
-    flag.Usage = func() {
-        fmt.Println("\n gdmk - makefile in pure go\n")
-        fmt.Printf(" usage: %s [OPTIONS] [TARGET]\n\n", os.Args[0])
-        fmt.Println(" options:\n")
-        fmt.Println("  -h --help         print this menu and exit")
-        fmt.Println("  -B --backend      choose backend [gc,gccgo,express]")
-        fmt.Println("  -o --output       link main package -> output")
-        fmt.Println("  -M --main         regex to match main package")
-        fmt.Println("  -c --clean        delete object files")
-        fmt.Println("  -q --quiet        quiet unless errors occur")
-        fmt.Println("  -e --external     goinstall external dependencies")
-        fmt.Println("  -I                import package directory\n")
-
-        if len(targets) > 0 {
-            fmt.Println(" targets:\n")
-            for k, v := range targets {
-                fmt.Printf("  %-11s  =>   %s\n", k, v.desc)
-            }
-            fmt.Println("")
-        }
-    }
-}
-
-func initBackend() {
-    switch backend {
-    case "gc":
-        n := archNum()
-        compiler, linker, suffix = n+"g", n+"l", "."+n
-    case "gccgo", "gcc":
-        compiler, linker, suffix = "gccgo", "gccgo", ".o"
-        backend = "gccgo"
-    case "express":
-        compiler, linker, suffix = "vmgc", "vmld", ".vmo"
-    default:
-        log.Fatalf("[ERROR] unknown backend: %s\n", backend)
-    }
-}
-
-func archNum() (n string) {
-    switch os.Getenv("GOARCH") {
-    case "386":
-        n = "8"
-    case "arm":
-        n = "5"
-    case "amd64":
-        n = "6"
-    default:
-        log.Fatalf("[ERROR] unknown GOARCH: %s\n", os.Getenv("GOARCH"))
-    }
-    return
-}
-
-
-//-------------------------------------------------------------------
-// External dependencies
-//-------------------------------------------------------------------
-
-var alien = []string{"launchpad.net/gobson/bson","launchpad.net/mgo","github.com/russross/blackfriday","github.com/cranej/session"}
-
-
-//-------------------------------------------------------------------
-// Functions to build/delete project
-//-------------------------------------------------------------------
-
-func osify(pkgs []*Package) {
-
-    for j := 0; j < len(pkgs); j++ {
-
-        if pkgs[j].osified {
-            break
-        }
-
-        pkgs[j].osified = true
-        pkgs[j].output = filepath.FromSlash(pkgs[j].output) + suffix
-        for i := 0; i < len(pkgs[j].files); i++ {
-            pkgs[j].files[i] = filepath.FromSlash(pkgs[j].files[i])
-        }
-    }
-}
-
-func mkdirs(pkgs []*Package) {
-    for i := 0; i < len(pkgs); i++ {
-        d, _ := filepath.Split(pkgs[i].output)
-        if d != "" && !isDir(d) {
-            e := os.MkdirAll(d, 0777)
-            if e != nil {
-                log.Fatalf("[ERROR] %s\n", e)
-            }
-        }
-    }
-}
-
-func compile(pkgs []*Package) {
-
-    osify(pkgs)
-    mkdirs(pkgs)
-
-    for i := 0; i < len(pkgs); i++ {
-        if oldPkgFound || !pkgs[i].up2date() {
-            say.Printf("compiling: %s\n", pkgs[i].full)
-            pkgs[i].compile()
-            oldPkgFound = true
-        } else {
-            say.Printf("up 2 date: %s\n", pkgs[i].full)
-        }
-    }
-}
-
-func delete(pkgs []*Package) {
-
-    osify(pkgs)
-
-    for j := 0; j < len(pkgs); j++ {
-        if isFile(pkgs[j].output) {
-            say.Printf("rm: %s\n", pkgs[j].output)
-            e := os.Remove(pkgs[j].output)
-            if e != nil {
-                log.Fatalf("[ERROR] failed to remove: %s\n", pkgs[j].output)
-            }
-        }
-    }
-
-    if emptyDir(includeDir){
-        say.Printf("rm: %s\n", includeDir)
-        e := os.RemoveAll(includeDir)
-        if e != nil {
-            log.Fatalf("[ERROR] failed to remove: %s\n", includeDir)
-        }
-    }
-
-}
-
-func link(pkgs []*Package) {
-
-    if output == "" {
-        return
-    }
-
-    var mainPackage *Package
-    var mainPkgs = make([]*Package, 0)
-
-    for i := 0; i < len(pkgs); i++ {
-        if pkgs[i].name == "main" {
-            mainPkgs = append(mainPkgs, pkgs[i])
-            mainPackage = pkgs[i]
-        }
-    }
-
-    switch len(mainPkgs) {
-    case 0:
-        log.Fatalf("[ERROR] no main package found\n")
-    case 1: // do nothing... this is good
-    default:
-        mainPackage = mainChoice(mainPkgs)
-    }
-
-    if !oldPkgFound && isFile(output) {
-        pkgLastTs, _ := timestamp(pkgs[len(pkgs)-1].output)
-        outputTs, ok := timestamp(output)
-        if ok && outputTs > pkgLastTs {
-            say.Printf("up 2 date: %s\n", output)
-            return
-        }
-    }
-
-    say.Printf("linking  : %s\n", output)
-
-    argv := make([]string, 0)
-    argv = append(argv, linker)
-
-    if backend != "gccgo" {
-
-        argv = append(argv, "-L")
-        argv = append(argv, includeDir)
-
-        if root != "" {
-            argv = append(argv, "-L")
-            argv = append(argv, root)
-        }
-    }
-
-    argv = append(argv, "-o")
-    argv = append(argv, output)
-
-    if backend == "gccgo" {
-        for i := 0; i < len(pkgs); i++ {
-            argv = append(argv, pkgs[i].output)
-        }
-        if root != "" {
-            errs := make(chan os.Error)
-            collect := &collector{make([]string, 0), nil}
-            collect.filter = func(s string)bool{
-                return strings.HasSuffix(s, ".o")
-            }
-            filepath.Walk(root, collect, errs)
-            for i := 0; i < len(collect.files); i++ {
-                argv = append(argv, collect.files[i])
-            }
-        }
-    }else{
-        argv = append(argv, mainPackage.output)
-    }
-
-    run(argv)
-}
-
-func mainChoice(pkgs []*Package) *Package {
-
-    var cnt, choice int
-
-    for i := 0; i < len(pkgs); i++ {
-        ok, _ := regexp.MatchString(match, pkgs[i].full)
-        if ok {
-            cnt++
-            choice = i
-        }
-    }
-
-    if cnt == 1 {
-        return pkgs[choice]
-    }
-
-    fmt.Println("\n More than one main package found\n")
-
-    for i := 0; i < len(pkgs); i++ {
-        fmt.Printf(" type %2d  for: %s\n", i, pkgs[i].full)
-    }
-
-    fmt.Printf("\n type your choice: ")
-
-    n, e := fmt.Scanf("%d", &choice)
-
-    if e != nil {
-        log.Fatalf("%s\n", e)
-    }
-    if n != 1 {
-        log.Fatal("failed to read input\n")
-    }
-
-    if choice >= len(pkgs) || choice < 0 {
-        log.Fatalf(" bad choice: %d\n", choice)
-    }
-
-    fmt.Printf(" chosen main-package: %s\n\n", pkgs[choice].full)
-
-    return pkgs[choice]
-}
-
-func goinstall() {
-
-    argv := make([]string, 4)
-    argv[0] = "goinstall"
-    argv[1] = "-clean=true"
-    argv[2] = "-u=true"
-
-    for i := 0; i < len(alien); i++ {
-        say.Printf("goinstall: %s\n", alien[i])
-        argv[3] = alien[i]
-        run(argv)
-    }
-}
-
-
-//-------------------------------------------------------------------
-// Utility types and functions
-//-------------------------------------------------------------------
-
-type collector struct{
-    files []string
-    filter func(string)bool
-}
-
-func (c *collector) VisitDir(pathname string, d *os.FileInfo) bool {
-    return true
-}
-
-func (c *collector) VisitFile(pathname string, d *os.FileInfo) {
-    if c.filter != nil {
-        if c.filter(pathname) {
-            c.files = append(c.files, pathname)
-        }
-    }else{
-        c.files = append(c.files, pathname)
-    }
-}
-
-func emptyDir(pathname string) bool {
-    if ! isDir(pathname) {
-        return false
-    }
-    errs := make(chan os.Error)
-    collect := &collector{make([]string, 0), nil}
-    filepath.Walk(pathname, collect, errs)
-    return len(collect.files) == 0
-}
-
-func isDir(pathname string) bool {
-    fileInfo, err := os.Stat(pathname)
-    if err == nil && fileInfo.IsDirectory() {
-        return true
-    }
-    return false
-}
-
-func timestamp(s string) (int64, bool) {
-    fileInfo, e := os.Stat(s)
-    if e == nil && fileInfo.IsRegular() {
-        return fileInfo.Mtime_ns, true
-    }
-    return 0, false
-}
-
-func run(argv []string) {
-
-    cmd := exec.Command(argv[0], argv[1:]...)
-
-    // pass-through
-    cmd.Stdout = os.Stdout
-    cmd.Stderr = os.Stderr
-    cmd.Stdin = os.Stdin
-
-    err := cmd.Start()
-
-    if err != nil {
-        log.Fatalf("[ERROR] %s\n", err)
-    }
-
-    err = cmd.Wait()
-
-    if err != nil {
-        log.Fatalf("[ERROR] %s\n", err)
-    }
-
-}
-
-func isFile(pathname string) bool {
-    fileInfo, err := os.Stat(pathname)
-    if err == nil && fileInfo.IsRegular() {
-        return true
-    }
-    return false
-}
-
-func quitter(e os.Error) {
-    if e != nil {
-        log.Fatalf("[ERROR] %s\n", e)
-    }
-}
-
-
-func copyGzipStringBuffer(from string, to string, gzipFile bool) {
-    buf := bytes.NewBufferString(from)
-    copyGzipReader(buf, to, gzipFile)
-}
-
-func copyGzipByteBuffer(from []byte, to string, gzipFile bool){
-    buf := bytes.NewBuffer(from)
-    copyGzipReader(buf, to, gzipFile)
-}
-
-func copyGzip(from, to string, gzipFile bool) {
-
-    var err os.Error
-    var fromFile *os.File
-
-    fromFile, err = os.Open(from)
-    quitter(err)
-
-    defer fromFile.Close()
-
-    copyGzipReader(fromFile, to, gzipFile)
-}
-
-func copyGzipReader(fromReader io.Reader, to string, gzipFile bool) {
-
-    var err os.Error
-    var toFile io.WriteCloser
-
-    toFile, err = os.Create(to)
-    quitter(err)
-
-    if gzipFile {
-        toFile, err = gzip.NewWriterLevel(toFile, gzip.BestCompression)
-        quitter(err)
-    }
-
-    defer toFile.Close()
-
-    _, err = io.Copy(toFile, fromReader)
-
-    quitter(err)
-}
-
-func listTargets() {
-    if list {
-        for k, _ := range targets {
-            fmt.Println(k)
-        }
-        os.Exit(0)
-    }
-}
-
-
-//-------------------------------------------------------------------
-// Package definition
-//-------------------------------------------------------------------
-
-type Package struct {
-    name, full, output string
-    osified            bool
-    files              []string
-}
-
-func (p *Package) up2date() bool {
-    mtime, ok := timestamp(p.output)
-    if !ok {
-        return false
-    }
-    for i := 0; i < len(p.files); i++ {
-        fmtime, ok := timestamp(p.files[i])
-        if !ok {
-            log.Fatalf("file missing: %s\n", p.files[i])
-        }
-        if fmtime > mtime {
-            return false
-        }
-    }
-    return true
-}
-
-func (p *Package) compile() {
-
-    argv := make([]string, 0)
-    argv = append(argv, compiler)
-    argv = append(argv, "-I")
-    argv = append(argv, includeDir)
-
-    if root != "" {
-        argv = append(argv, "-I")
-        argv = append(argv, root)
-    }
-    if backend == "gccgo" {
-        argv = append(argv, "-c")
-    }
-    argv = append(argv, "-o")
-    argv = append(argv, p.output)
-    argv = append(argv, p.files...)
-
-    run(argv)
-
-}
-
-
-//-------------------------------------------------------------------
-// Package info collected by godag
-//-------------------------------------------------------------------
-
-var packages = []*Package{
-    &Package{
-        name:   "config",
-        full:    "lib/config",
-        output: "_obj/lib/config",
-        files:  []string{"src/lib/config.go"},
-    },
-    &Package{
-        name:   "utility",
-        full:    "lib/utility",
-        output: "_obj/lib/utility",
-        files:  []string{"src/lib/utility.go"},
-    },
-    &Package{
-        name:   "blog",
-        full:    "lib/blog",
-        output: "_obj/lib/blog",
-        files:  []string{"src/lib/blog.go"},
-    },
-    &Package{
-        name:   "syncedtpl",
-        full:    "lib/syncedtpl",
-        output: "_obj/lib/syncedtpl",
-        files:  []string{"src/lib/syncedtpl.go"},
-    },
-    &Package{
-        name:   "user",
-        full:    "lib/user",
-        output: "_obj/lib/user",
-        files:  []string{"src/lib/user.go"},
-    },
-    &Package{
-        name:   "word",
-        full:    "lib/word",
-        output: "_obj/lib/word",
-        files:  []string{"src/lib/word.go"},
-    },
-    &Package{
-        name:   "main",
-        full:    "main",
-        output: "_obj/main",
-        files:  []string{"src/mysite.go"},
-    },
-
-}
-
-//-------------------------------------------------------------------
-// Main - note flags are parsed before we doFirst, i.e. command line
-//        options/arguments can be overridden in doFirst targets
-//-------------------------------------------------------------------
-
-func main() {
-
-    flag.Parse()
-    listTargets() // for bash auto complete
-    initBackend() // gc/gcc/express
-
-    if quiet {
-        say = Say(false)
-    }
-
-    doFirst()
-    defer doLast()
-
-    switch {
-    case help:
-        flag.Usage()
-    case clean:
-        delete(packages)
-    case external:
-        goinstall()
-    default:
-        compile(packages)
-        link(packages)
-    }
-
-}

model/blog/blog.go

+package blog
+
+import (
+	"errors"
+	"fmt"
+	"launchpad.net/mgo/bson"
+	"launchpad.net/mgo"
+	"chunhe.info/config"
+	"time"
+)
+
+type Blog struct {
+	Id               bson.ObjectId "_id,omitempty"
+	CreatedDate      int64
+	LastModifiedDate int64
+	Title            string
+	Body             string
+	Tags             []string
+	Slug             string
+	Comments         []Comment
+}
+
+type Comment struct {
+	Id          bson.ObjectId "_id,omitempty"
+	Author      string
+	Email       string
+	WebSite     string
+	Content     string
+	CreatedDate int64
+}
+
+func NewComment(author, email, content, webSite string) *Comment {
+	return &Comment{Id: bson.NewObjectId(), Author: author, Email: email, WebSite: webSite, Content: content, CreatedDate: time.Now().UnixNano()}
+}
+
+var (
+	db                string
+	mapReduceInterval time.Duration
+	session           *mgo.Session
+	job               = mgo.MapReduce{
+		Map:    "function(){if(!this.tags) return; for(i in this.tags){emit(this.tags[i],1);}}",
+		Reduce: "function(key, values) {var sum = 0; for(i in values){sum = sum + values[i];} return sum;}",
+		Out:    bson.M{"replace": "tags"},
+	}
+	job_en = mgo.MapReduce{
+		Map:    "function(){if(!this.tags) return; for(i in this.tags){emit(this.tags[i],1);}}",
+		Reduce: "function(key, values) {var sum = 0; for(i in values){sum = sum + values[i];} return sum;}",
+		Out:    bson.M{"replace": "tags_en"},
+	}
+)
+
+func init() {
+	var err error
+	var serverHost string
+
+	if !config.HasLoaded() {
+		err = config.LoadConfig("config.json")
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	serverHost, err = config.GetString("Server")
+	if err != nil {
+		panic(err)
+	}
+
+	db, err = config.GetString("DB")
+	if err != nil {
+		panic(err)
+	}
+
+	mapReduceInterval, err = config.GetDuration("MapReduceInterval")
+	if err != nil {
+		panic(err)
+	}
+	
+	session, err = mgo.Dial(serverHost)
+	if err != nil {
+		panic(err)
+	}
+	session.SetMode(mgo.Monotonic, true)
+}
+
+func getCName(name, lang string) string {
+	if lang != "" {
+		return name + "_" + lang
+	}
+
+	return name
+}
+
+func GetBlogBySlug(slug, lang string) (*Blog, error) {
+	result := Blog{}
+	c := session.DB(db).C(getCName("blog", lang))
+	err := c.Find(bson.M{"slug": slug}).One(&result)
+
+	return &result, err
+}
+
+func GetBlogById(id, lang string) (*Blog, error) {
+	result := Blog{}
+	c := session.DB(db).C(getCName("blog", lang))
+	err := c.Find(bson.M{"_id": bson.ObjectIdHex(id)}).One(&result)
+
+	return &result, err
+}
+
+func GetBlogsByPage(page, pageSize int, lang string) ([]Blog, error) {
+	if page <= 0 || pageSize <= 0 {
+		return nil, errors.New("page and pageSize must be greater than 0.")
+	}
+	result := make([]Blog, 0, pageSize)
+	c := session.DB(db).C(getCName("blog", lang))
+	err := c.Find(nil).Sort(bson.M{"createddate": -1}).Skip((page - 1) * pageSize).Limit(pageSize).All(&result)
+
+	return result, err
+}
+
+func GetBlogsCount(lang string) (int, error) {
+	c := session.DB(db).C(getCName("blog", lang))
+	return c.Count()
+}
+
+func GetBlogsByTag(tag, lang string) ([]Blog, error) {
+	//TODO: paging
+	result := make([]Blog, 0, 10)
+	c := session.DB(db).C(getCName("blog", lang))
+	err := c.Find(bson.M{"tags": tag}).Sort(bson.M{"createddate": -1}).All(&result)
+
+	return result, err
+}
+
+//TODO: localize tag data
+func CollectTags() {
+	c := session.DB(db).C("blog")
+	_, err := c.Find(nil).MapReduce(job, nil)
+	if err != nil {
+		//TODO: better logger
+		fmt.Println(err)
+	}
+
+	c = session.DB(db).C("blog_en")
+	_, err = c.Find(nil).MapReduce(job_en, nil)
+	if err != nil {
+		//TODO: better logger
+		fmt.Println(err)
+	}
+	time.AfterFunc(mapReduceInterval, CollectTags)
+}
+
+func GetTagCloud(lang string) (map[string]float32, error) {
+	var result []struct {
+		Id    string "_id"
+		Value float32
+	}
+	c := session.DB(db).C(getCName("tags", lang))
+	err := c.Find(nil).All(&result)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(result) == 0 {
+		return nil, nil
+	}
+
+	m := make(map[string]float32)
+	max := float32(0)
+	min := max
+	for _, v := range result {
+		if v.Value > max {
+			max = v.Value
+		}
+
+		if v.Value < min {
+			min = v.Value
+		}
+
+		m[v.Id] = v.Value
+	}
+
+	base := max - min
+	for k, v := range m {
+		m[k] = (v - min) / base
+	}
+
+	return m, nil
+}
+
+func AddOrUpdateBlog(blog *Blog, lang string) error {
+	c := session.DB(db).C(getCName("blog", lang))
+	_, err := c.Upsert(bson.M{"slug": blog.Slug}, blog)
+	return err
+}
+
+func Close() {
+	if session != nil {
+		session.Close()
+	}
+}
+
+type Feeds struct {
+	LastBuildDate int64
+	Items         []Blog
+}
+
+func GetRSS(limit int, lang string) (Feeds, error) {
+	result := make([]Blog, 0, limit)
+	c := session.DB(db).C(getCName("blog", lang))
+	err := c.Find(nil).Select(bson.M{"title": 1, "body": 1, "createddate": 1, "slug": 1}).Sort(bson.M{"createddate": -1}).Limit(limit).All(&result)
+	return Feeds{LastBuildDate: time.Now().UnixNano(), Items: result}, err
+}
+
+func NewBlog(title, body, slug string, tags []string) *Blog {
+	now := time.Now().UnixNano()
+	return &Blog{CreatedDate: now, LastModifiedDate: now, Title: title, Body: body, Tags: tags, Slug: slug}
+}
+
+func (blog *Blog) Save(lang string) error {
+	return AddOrUpdateBlog(blog, lang)
+}
+
+func (blog *Blog) Update(lang string) error {
+	blog.LastModifiedDate = time.Now().UnixNano()
+	return AddOrUpdateBlog(blog, lang)
+}

model/user/user.go

+package user
+
+import (
+	"launchpad.net/mgo/bson"
+	"launchpad.net/mgo"
+	"chunhe.info/config"
+	"chunhe.info/utility"
+)
+
+type User struct {
+	Id              bson.ObjectId "_id,omitempty"
+	Username        string
+	Email           string
+	WordsSyncedDate string
+}
+
+var (
+	session *mgo.Session
+	db      string
+)
+
+func init() {
+	var err error
+	var serverHost string
+
+	if !config.HasLoaded() {
+		err = config.LoadConfig("config.json")
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	serverHost, err = config.GetString("Server")
+	if err != nil {
+		panic(err)
+	}
+
+	db, err = config.GetString("DB")
+	if err != nil {
+		panic(err)
+	}
+
+	session, err = mgo.Dial(serverHost)
+	if err != nil {
+		panic(err)
+	}
+	session.SetMode(mgo.Monotonic, true)
+}
+
+func FetchUser(username, password string) (*User, error) {
+	p := utility.GetMd5Hex(password)
+	c := session.DB(db).C("user")
+	var user User
+	err := c.Find(bson.M{"username": username, "password": p}).Select(bson.M{"_id": 1, "username": 1, "email": 1, "wordssynceddate": 1}).One(&user)
+	if err != nil {
+		return nil, err
+	}
+
+	return &user, nil
+}
+
+func UpdateWordsSyncedDate(uid, date string) error {
+	c := session.DB(db).C("user")
+	err := c.Update(bson.M{"_id": bson.ObjectIdHex(uid)}, bson.M{"$set": bson.M{"wordssynceddate": date}})
+	if err == mgo.NotFound {
+		return nil
+	}
+	return err
+}

model/word/word.go

+package word
+
+import (
+	"encoding/json"
+	"fmt"
+	"launchpad.net/mgo/bson"
+	"launchpad.net/mgo"
+	"chunhe.info/config"
+	"chunhe.info/model/user"
+	"chunhe.info/utility"
+	"sort"
+)
+
+type word struct {
+	Word             string
+	Days             []string
+	Scenes           []string
+	Uid              string `json:",omitempty"`
+	LastModifiedDate string
+}
+
+type WordExplain struct {
+	Word    string
+	Scenes  []string
+	Explain string
+	Times   int
+}
+
+var (
+	session *mgo.Session
+	db      string
+)
+
+func init() {
+	var err error
+	var serverHost string
+
+	if !config.HasLoaded() {
+		err = config.LoadConfig("config.json")
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	serverHost, err = config.GetString("Server")
+	if err != nil {
+		panic(err)
+	}
+
+	db, err = config.GetString("DB")
+	if err != nil {
+		panic(err)
+	}
+
+	session, err = mgo.Dial(serverHost)
+	if err != nil {
+		panic(err)
+	}
+	session.SetMode(mgo.Monotonic, true)
+}
+
+func GetWordsByDay(day, uid string) ([]*WordExplain, error) {
+	var wordsList []word
+	c := session.DB(db).C("wordsstat")
+	err := c.Find(bson.M{"days": day, "uid": uid}).All(&wordsList)
+	if err != nil {
+		return nil, err
+	}
+
+	wordsCount := make(map[string]*WordExplain)
+	words := make([]string, 0, len(wordsList))
+	for _, w := range wordsList {
+		words = append(words, w.Word)
+		we := new(WordExplain)
+		we.Times = len(w.Days)
+		we.Word = w.Word
+		we.Scenes = w.Scenes
+		wordsCount[w.Word] = we
+	}
+
+	result := make([]*WordExplain, 0, len(wordsList))
+	var explain []struct {
+		WordString  string
+		WordExplain string
+	}
+	c = session.DB("dict").C("oxfordformated")
+	err = c.Find(bson.M{"wordstring": bson.M{"$in": words}}).All(&explain)
+	for _, e := range explain {
+		w, _ := wordsCount[e.WordString]
+		w.Explain = e.WordExplain
+		result = append(result, w)
+		delete(wordsCount, e.WordString)
+	}
+
+	for _, v := range wordsCount {
+		result = append(result, v)
+	}
+
+	return result, err
+}
+
+func AddWord(wordstring, scenes, uid string) error {
+	c := session.DB(db).C("wordsstat")
+	t := utility.NowOffset(8) //current BEIJING time
+	today := fmt.Sprintf("%d-%02d-%02d", t.Year(), t.Month(), t.Day())
+	var w word
+	err := c.Find(bson.M{"word": wordstring, "uid": uid}).One(&w)
+	if err == mgo.NotFound {
+		//Not Found record, insert
+		newWord := &word{wordstring, []string{today}, []string{}, uid, today}
+		if scenes != "" {
+			newWord.Scenes = append(newWord.Scenes, scenes)
+		}
+		err = c.Insert(newWord)
+	} else if err == nil {
+		//Found record 
+		sort.Strings(w.Days)
+		if i := sort.SearchStrings(w.Days, today); i >= len(w.Days) || w.Days[i] != today {
+			//no such date in w.Days, update 
+			w.Days = append(w.Days, today)
+			w.LastModifiedDate = today
+		}
+
+		sort.Strings(w.Scenes)
+		if i := sort.SearchStrings(w.Scenes, scenes); i >= len(w.Scenes) || w.Scenes[i] != scenes {
+			//update scenes
+			w.Scenes = append(w.Scenes, scenes)
+			w.LastModifiedDate = today
+		}
+
+		err = c.Update(bson.M{"word": wordstring, "uid": uid}, &w)
+	}
+
+	return err
+}
+
+func EditWord(oldWordString, newWordString, date, uid string) error {
+	t := utility.NowOffset(8) //current BEIJING time
+	today := fmt.Sprintf("%d-%02d-%02d", t.Year(), t.Month(), t.Day())
+	c := session.DB(db).C("wordsstat")
+	err := c.Update(bson.M{"word": oldWordString, "days": date, "uid": uid}, bson.M{"$set": bson.M{"word": newWordString, "lastmodifieddate": today}})
+	return err
+}
+
+//TODO: need to avoid returning all dates every time for efficient memory useage when being stored in session.
+func GetAvailableDays(uid string) ([]string, error) {
+	var result = make([]string, 0, 100)
+	c := session.DB(db).C("wordsstat")
+	err := c.Find(bson.M{"uid": uid}).Distinct("days", &result)
+	sort.Strings(result)
+	//reverse to sort descing
+	for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
+		result[i], result[j] = result[j], result[i]
+	}
+	return result, err
+}
+
+func ImportFromJson(dataInJson, uid, wordssynceddate string) error {
+	data := make([]word, 0, 100)
+	err := json.Unmarshal([]byte(dataInJson), &data)
+	if err != nil {
+		return err
+	}
+	c := session.DB(db).C("wordsstat")
+	var lastImportedDate string
+	for _, v := range data {
+		//TODO: should only import words whose lastmodifieddate < current value?
+		if v.LastModifiedDate == "" || v.LastModifiedDate < wordssynceddate {
+			continue
+		}
+
+		if v.LastModifiedDate > lastImportedDate {
+			lastImportedDate = v.LastModifiedDate
+		}
+
+		v.Uid = uid
+		_, err = c.Upsert(bson.M{"word": v.Word, "uid": uid}, &v)
+		if err != nil {
+			return err
+		}
+	}
+
+	if lastImportedDate != "" {
+		err = user.UpdateWordsSyncedDate(uid, lastImportedDate)
+		return err
+	}
+
+	return nil
+}
+
+//TODO: May need a startDate parameter
+func ExportJson(uid string) (string, error) {
+	var data []word
+	c := session.DB(db).C("wordsstat")
+	err := c.Find(bson.M{"uid": uid}).Select(bson.M{"word": 1, "days": 1, "scenes": 1, "lastmodifieddate": 1}).All(&data)
+	if err == mgo.NotFound {
+		return "null", nil
+	} else if err != nil {
+		return "", err
+	}
+
+	result, jerr := json.Marshal(&data)
+	if jerr != nil {
+		return "", jerr
+	}
+
+	return string(result), nil
+}
+package main
+
+import (
+	"flag"
+	"fmt"
+	"github.com/cranej/session"
+	"io/ioutil"
+	"chunhe.info/config"
+	"chunhe.info/utility"
+	"chunhe.info/model/user"
+	"chunhe.info/model/blog"
+	"chunhe.info/model/word"
+	"log"
+	"net/http"
+	"os"
+	"strconv"
+	"strings"
+	"github.com/hoisie/web"
+	"time"
+)
+
+var (
+	port            = flag.Int("p", 80, "Port number to listen at.")
+	pages, parseErr = NewTemplateSet()
+	pageSize        int
+	maxTagFont      float32
+	minTagFont      float32
+	sessionTimeout  time.Duration
+)
+
+func init() {
+	var err error
+	pageSize, err = config.GetInt("PageSize")
+	if err != nil {
+		panic(err)
+	}
+	maxTagFont, err = config.GetFloat32("MaxTagFont")
+	if err != nil {
+		panic(err)
+	}
+	minTagFont, err = config.GetFloat32("MinTagFont")
+	if err != nil {
+		panic(err)
+	}
+	sessionTimeout, err = config.GetDuration("SessionTimeout")
+	if err != nil {
+		panic(err)
+	}
+
+}
+
+func indexPage(ctx *web.Context, lang string) {
+	//pages.Execute(ctx, "index", nil)
+	var blogsBaseUrl string
+	if lang != "" {
+		blogsBaseUrl = "/" + lang + "blogs"
+	} else {
+		blogsBaseUrl = "/blogs"
+	}
+
+	ctx.Redirect(http.StatusFound, blogsBaseUrl)
+}
+
+type BlogListData struct {
+	Blogs        []blog.Blog
+	CurrentPage  int
+	PreviousPage int
+	NextPage     int
+	BlogsCount   int
+	TagCloud     map[string]float32
+}
+
+func getLocalizedTplName(name, lang string) string {
+	if lang != "" {
+		return name + "-" + lang
+	}
+
+	return name
+}
+
+func getBlogsByPage(ctx *web.Context, lang, page string) {
+	if lang != "" {
+		//remove last character '/'
+		lang = string(lang[0 : len(lang)-1])
+	}
+
+	pageN, _ := strconv.Atoi(page)
+	blogs, err := blog.GetBlogsByPage(pageN, pageSize, lang)
+	if err != nil {
+		renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+	} else {
+		blogsCount, err2 := blog.GetBlogsCount(lang)
+		if err2 != nil {
+			renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+			return
+		}
+
+		tagCloud, err3 := blog.GetTagCloud(lang)
+		if err3 != nil {
+			renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+			return
+		}
+
+		for k, v := range tagCloud {
+			fs := v * maxTagFont
+			if fs < minTagFont {
+				fs = minTagFont
+			}
+			tagCloud[k] = fs
+		}
+
+		previousPage := 0
+		nextPage := 0
+		if (pageN-1)*pageSize+len(blogs) < blogsCount {
+			nextPage = pageN + 1
+		}
+		if pageN > 1 {
+			previousPage = pageN - 1
+		}
+		pages.Execute(ctx, getLocalizedTplName("blogs", lang), BlogListData{blogs, pageN, previousPage, nextPage, blogsCount, tagCloud})
+	}
+}
+
+func getBlogs(ctx *web.Context, lang string) {
+	getBlogsByPage(ctx, lang, "1")
+}
+
+func aboutPage(ctx *web.Context) {
+	pages.Execute(ctx, "about", nil)
+}
+
+func getBlogBySlug(ctx *web.Context, lang, slug string) {
+	if lang != "" {
+		//remove last character '/'
+		lang = string(lang[0 : len(lang)-1])
+	}
+
+	b, err := blog.GetBlogBySlug(slug, lang)
+	if err == nil {
+		pages.Execute(ctx, getLocalizedTplName("blog", lang), b)
+	} else {
+		renderError(ctx, http.StatusNotFound, []string{"Page not found."})
+	}
+}
+
+func getBlogsByTag(ctx *web.Context, lang, tag string) {
+	if lang != "" {
+		//remove last character '/'
+		lang = string(lang[0 : len(lang)-1])
+	}
+
+	//TODO:paging
+	blogs, err := blog.GetBlogsByTag(tag, lang)
+	if err != nil {
+		renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+	} else {
+		pages.Execute(ctx, getLocalizedTplName("blogs", lang), BlogListData{blogs, 0, 0, 0, 0, nil})
+	}
+}
+
+func getFeed(ctx *web.Context, lang string) {
+	if lang != "" {
+		//remove last character '/'
+		lang = string(lang[0 : len(lang)-1])
+	}
+
+	feeds, err := blog.GetRSS(50, lang)
+	if err != nil {
+		renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+	} else {
+		ctx.SetHeader("Content-Type", "text/xml", true)
+		pages.Execute(ctx, getLocalizedTplName("rss", lang), feeds)
+	}
+}
+
+func getWordsByDate(ctx *web.Context, year, month, day string) {
+	loggedIn, sid := checkUser(ctx)
+	if !loggedIn {
+		ctx.Redirect(http.StatusFound, getLoginUrl(ctx))
+		return
+	}
+
+	y, _ := strconv.Atoi(year)
+	m, _ := strconv.Atoi(month)
+	d, _ := strconv.Atoi(day)
+
+	var data struct {
+		Words []struct {
+			Item         *word.WordExplain
+			RatingOffset int
+			Weight       int
+		}
+		Date string
+	}
+
+	date := fmt.Sprintf("%d-%02d-%02d", y, m, d)
+	u, _ := session.SessionFromId(sid).Get("user")
+	uobj := u.(*user.User)
+
+	words, err := word.GetWordsByDay(date, uobj.Id.Hex())
+	if err != nil {
+		renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+	} else {
+		tempList := make([]struct {
+			Item         *word.WordExplain
+			RatingOffset int
+			Weight       int
+		}, 0, len(words))
+		for _, w := range words {
+			var tempW struct {
+				Item         *word.WordExplain
+				RatingOffset int
+				Weight       int
+			}
+			tempW.Item = w
+			tempW.RatingOffset = 0 - (75 - 15*w.Times)
+			tempW.Weight = w.Times
+			//show at most 5 stars
+			if tempW.Weight > 5 {
+				tempW.RatingOffset = 0
+			}
+			//don't show stars when weight = 1
+			if tempW.Weight == 1 {
+				tempW.RatingOffset = -75
+			}
+			tempList = append(tempList, tempW)
+		}
+
+		data.Date = date
+		data.Words = tempList
+		pages.Execute(ctx, "words", data)
+	}
+}
+
+func getDatesForWords(ctx *web.Context) {
+	loggedIn, sid := checkUser(ctx)
+	if !loggedIn {
+		ctx.Redirect(http.StatusFound, getLoginUrl(ctx))
+		return
+	}
+
+	u, _ := session.SessionFromId(sid).Get("user")
+	uobj := u.(*user.User)
+
+	dates, err := word.GetAvailableDays(uobj.Id.Hex())
+	if err != nil {
+		renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+	} else {
+		pages.Execute(ctx, "wordslist", dates)
+	}
+}
+
+func addWord(ctx *web.Context) {
+	loggedIn, sid := checkUser(ctx)
+	if !loggedIn {
+		renderError(ctx, http.StatusUnauthorized, []string{"Unauthorized"})
+		return
+	}
+
+	var wordString, wordScenes string
+	errMsgs := make([]string, 0, 2)
+	wordString, errMsgs = getParam(ctx, "wordString", errMsgs, true)
+	wordScenes, errMsgs = getParam(ctx, "wordScenes", errMsgs, false)
+	wordScenes = strings.Trim(wordScenes, " ")
+	if len(errMsgs) != 0 {
+		renderError(ctx, http.StatusBadRequest, errMsgs)
+	} else {
+		u, _ := session.SessionFromId(sid).Get("user")
+		uobj := u.(*user.User)
+
+		err := word.AddWord(wordString, wordScenes, uobj.Id.Hex())
+		if err != nil {
+			renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+		} else {
+			ctx.WriteString("ok")
+		}
+	}
+}
+
+func editWord(ctx *web.Context, year, month, day string) {
+	loggedIn, sid := checkUser(ctx)
+	if !loggedIn {
+		renderError(ctx, http.StatusUnauthorized, []string{"Unauthorized"})
+		return
+	}
+
+	y, _ := strconv.Atoi(year)
+	m, _ := strconv.Atoi(month)
+	d, _ := strconv.Atoi(day)
+
+	date := fmt.Sprintf("%d-%02d-%02d", y, m, d)
+
+	var oldWordString, newWordString string
+	errMsgs := make([]string, 0, 2)
+	oldWordString, errMsgs = getParam(ctx, "oldWordString", errMsgs, true)
+	newWordString, errMsgs = getParam(ctx, "newWordString", errMsgs, true)
+
+	if len(errMsgs) != 0 {
+		renderError(ctx, http.StatusBadRequest, errMsgs)
+	} else {
+		u, _ := session.SessionFromId(sid).Get("user")
+		uobj := u.(*user.User)
+
+		err := word.EditWord(oldWordString, newWordString, date, uobj.Id.Hex())
+		if err != nil {
+			renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+		} else {
+			ctx.WriteString("ok")
+		}
+	}
+}
+
+func importWords(ctx *web.Context) {
+	loggedIn, sid := checkUser(ctx)
+	if !loggedIn {
+		renderError(ctx, http.StatusUnauthorized, []string{"Unauthorized"})
+		return
+	}
+	var wordsData string
+	errMsgs := make([]string, 0, 1)
+	wordsData, errMsgs = getParam(ctx, "wordsData", errMsgs, true)
+
+	if len(errMsgs) != 0 {
+		renderError(ctx, http.StatusBadRequest, errMsgs)
+	} else {
+		u, _ := session.SessionFromId(sid).Get("user")
+		uobj := u.(*user.User)
+
+		err := word.ImportFromJson(wordsData, uobj.Id.Hex(), uobj.WordsSyncedDate)
+		if err != nil {
+			renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+		} else {
+			ctx.WriteString("ok")
+		}
+	}
+}
+
+func exportWords(ctx *web.Context) {
+	loggedIn, sid := checkUser(ctx)
+	if !loggedIn {
+		renderError(ctx, http.StatusUnauthorized, []string{"Unauthorized"})
+		return
+	}
+	u, _ := session.SessionFromId(sid).Get("user")
+	uobj := u.(*user.User)
+
+	result, err := word.ExportJson(uobj.Id.Hex())
+	if err != nil {
+		renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+	} else {
+		ctx.WriteString(result)
+	}
+}
+
+func blogForm(ctx *web.Context, lang, action string) {
+	if lang != "" {
+		//remove last character '/'
+		lang = string(lang[0 : len(lang)-1])
+	}
+
+	loggedIn, _ := checkUser(ctx)
+	if !loggedIn {
+		ctx.Redirect(http.StatusFound, getLoginUrl(ctx))
+		return
+	}
+
+	if action == "edit" {
+		slug, errMsgs := getParam(ctx, "slug", make([]string, 0, 1), true)
+		if len(errMsgs) == 0 {
+			b, err := blog.GetBlogBySlug(slug, lang)
+			if err == nil {
+				pages.Execute(ctx, getLocalizedTplName("blogform", lang), b)
+			} else {
+				renderError(ctx, http.StatusNotFound, []string{"Page not found."})
+			}
+		} else {
+			renderError(ctx, http.StatusBadRequest, errMsgs)
+		}
+	} else {
+		pages.Execute(ctx, getLocalizedTplName("blogform", lang), nil)
+	}
+}
+
+func addBlog(ctx *web.Context, lang string) {
+	var blogsUrlBase string
+	if lang != "" {
+		//remove last character '/'
+		lang = string(lang[0 : len(lang)-1])
+		blogsUrlBase = "/" + lang + "/blogs/"
+	} else {
+		blogsUrlBase = "/blogs/"
+	}
+	var title, body, slug, tags string
+	if loggedIn, _ := checkUser(ctx); !loggedIn {
+		ctx.Redirect(http.StatusFound, getLoginUrl(ctx))
+		return
+	}
+
+	errMsgs := make([]string, 0, 4)
+	title, errMsgs = getParam(ctx, "blogTitle", errMsgs, true)
+	body, errMsgs = getParam(ctx, "blogBody", errMsgs, true)
+	slug, errMsgs = getParam(ctx, "blogSlug", errMsgs, true)
+	tags, errMsgs = getParam(ctx, "blogTags", errMsgs, true)
+
+	if len(errMsgs) == 0 {
+		b := blog.NewBlog(title, body, slug, strings.Split(tags, ","))
+		err := b.Save(lang)
+		if err != nil {
+			renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+		} else {
+			ctx.Redirect(http.StatusFound, blogsUrlBase+b.Slug)
+		}
+
+	} else {
+		renderError(ctx, http.StatusBadRequest, errMsgs)
+	}
+}
+
+func editBlog(ctx *web.Context, lang, slug string) {
+	var blogsUrlBase string
+	if lang != "" {
+		//remove last character '/'
+		lang = string(lang[0 : len(lang)-1])
+		blogsUrlBase = "/" + lang + "/blogs/"
+	} else {
+		blogsUrlBase = "/blogs/"
+	}
+	var title, body, tags string
+	if loggedIn, _ := checkUser(ctx); !loggedIn {
+		ctx.Redirect(http.StatusFound, getLoginUrl(ctx))
+		return
+	}
+
+	errMsgs := make([]string, 0, 3)
+	title, errMsgs = getParam(ctx, "blogTitle", errMsgs, true)
+	body, errMsgs = getParam(ctx, "blogBody", errMsgs, true)
+	tags, errMsgs = getParam(ctx, "blogTags", errMsgs, true)
+
+	if len(errMsgs) == 0 {
+		b, err := blog.GetBlogBySlug(slug, lang)
+		if err == nil {
+			b.Title = title
+			b.Body = body
+			b.Tags = strings.Split(tags, ",")
+			err = b.Update(lang)
+			if err == nil {
+				ctx.Redirect(http.StatusFound, blogsUrlBase+b.Slug)
+			} else {
+				renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+			}
+		} else {
+			renderError(ctx, http.StatusNotFound, []string{"Blog not found"})
+		}
+	} else {
+		renderError(ctx, http.StatusBadRequest, errMsgs)
+	}
+}
+
+func addBlogComment(ctx *web.Context, lang string) {
+	var blogsUrlBase string
+	if lang != "" {
+		//remove last character '/'
+		lang = string(lang[0 : len(lang)-1])
+		blogsUrlBase = "/" + lang + "/blogs/"
+	} else {
+		blogsUrlBase = "/blogs/"
+	}
+
+	var author, email, content, webSite, blogId string
+	errMsgs := make([]string, 0, 4)
+	author, errMsgs = getParam(ctx, "commentAuthor", errMsgs, true)
+	email, errMsgs = getParam(ctx, "commentEmail", errMsgs, true)
+	content, errMsgs = getParam(ctx, "commentContent", errMsgs, true)
+	blogId, errMsgs = getParam(ctx, "commentBlogId", errMsgs, true)
+	webSite, errMsgs = getParam(ctx, "commentWebSite", errMsgs, false)
+
+	if len(errMsgs) == 0 {
+		b, err := blog.GetBlogById(blogId, lang)
+		if err == nil {
+			b.Comments = append(b.Comments, *blog.NewComment(author, email, content, webSite))
+			b.Save(lang)
+			ctx.Redirect(http.StatusFound, blogsUrlBase+b.Slug)
+		} else {
+			renderError(ctx, http.StatusNotFound, []string{"Page not found."})
+		}
+	} else {
+		renderError(ctx, http.StatusBadRequest, errMsgs)
+	}
+}
+
+func refreshTpl(ctx *web.Context, lang string) {
+	var redirectTo string
+	if lang != "" {
+		redirectTo = "/" + lang + "admin"
+	} else {
+		redirectTo = "/admin"
+	}
+
+	if loggedIn, _ := checkUser(ctx); !loggedIn {
+		ctx.Redirect(http.StatusFound, getLoginUrl(ctx))
+		return
+	}
+
+	recompileTemplate()
+	ctx.Redirect(http.StatusFound, redirectTo)
+}
+
+func loginForm(ctx *web.Context) {
+	url, _ := ctx.Params["url"]
+	pages.Execute(ctx, "login", url)
+}
+
+func adminPage(ctx *web.Context, lang string) {
+	if lang != "" {
+		//remove last character '/'
+		lang = string(lang[0 : len(lang)-1])
+	}
+
+	if loggedIn, _ := checkUser(ctx); !loggedIn {
+		ctx.Redirect(http.StatusFound, getLoginUrl(ctx))
+		return
+	}
+
+	pages.Execute(ctx, getLocalizedTplName("admin", lang), lang)
+}
+
+func getLoginUrl(ctx *web.Context) string {
+	return "/login?url=" + ctx.Request.URL.Path
+}
+
+func login(ctx *web.Context) {
+	var username, password string
+	errMsgs := make([]string, 0, 2)
+	username, errMsgs = getParam(ctx, "username", errMsgs, true)
+	password, errMsgs = getParam(ctx, "password", errMsgs, true)
+
+	if len(errMsgs) == 0 {
+		u, err := user.FetchUser(username, password)
+		if err != nil {
+			renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+			return
+		}
+
+		if u == nil {
+			renderError(ctx, http.StatusForbidden, []string{"Login failed."})
+			return
+		}
+
+		//TODO: should logout user first if there is an none expired session
+		s := session.NewTimeout(sessionTimeout)
+		ctx.SetCookie("u", string(s), 3600)
+		err = s.Put("user", u)
+		if err != nil {
+			renderError(ctx, http.StatusInternalServerError, []string{err.Error()})
+			return
+		}
+
+		url, ok := ctx.Params["url"]
+		if !ok || url == "" {
+			url = "/blogs"
+		}
+		ctx.Redirect(http.StatusFound, url)
+
+	} else {
+		renderError(ctx, http.StatusBadRequest, errMsgs)
+	}
+}
+
+func checkUser(ctx *web.Context) (bool, string) {
+	ok := false
+	var sid string
+	for _, c := range ctx.Request.Cookies() {
+		if c.Name != "u" {
+			continue
+		}
+
+		if session.SessionFromId(c.Value).Exists() {
+			ok = true
+			sid = c.Value
+			break
+		}
+
+	}
+	return ok, sid
+}
+
+func main() {
+	if parseErr != nil {
+		fmt.Println(parseErr)
+		fmt.Println(`Failed to parse template files.`)
+		return
+	}
+
+	defer blog.Close()
+	f, logFileErr := os.OpenFile("server.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
+	if logFileErr != nil {
+		fmt.Println(logFileErr)
+		return
+	}
+
+	logger := log.New(f, "", log.Ldate|log.Ltime)
+	web.SetLogger(logger)
+
+	web.Config.CookieSecret = utility.GetSecretKey()
+
+	go blog.CollectTags()
+	flag.Parse()
+	//GET
+	web.Get(`/(en/)?blogs/feed/?$`, getFeed)
+	web.Get(`/(en/)?blogs/page-([1-9][0-9]*)/?$`, getBlogsByPage)
+	web.Get(`/(en/)?blogs/([a-zA-Z0-9_\-]+)/?$`, getBlogBySlug)
+	web.Get(`/(en/)?blogs/tags/([^/]+)/?$`, getBlogsByTag)
+	web.Get(`/(en/)?blogs/?$`, getBlogs)
+	web.Get(`/about/?$`, aboutPage)
+	web.Get(`/login/?$`, loginForm)
+	web.Get(`/(en/)?admin/?$`, adminPage)
+	web.Get(`/(en/)?admin/(create|edit)-blog/?$`, blogForm)
+	web.Get(`/(en/)?admin/refreshtpl/?$`, refreshTpl)
+	web.Get(`/(en/)?$`, indexPage)
+	web.Get(`/words/(2[0-9][1-9][0-9])-([0-1][0-9])-([0-3][0-9])/?$`, getWordsByDate)
+	web.Get(`/words/?$`, getDatesForWords)
+
+	//POST
+	web.Post(`/(en/)?blogs$`, addBlog)
+	web.Post(`/(en/)?blogs/([a-zA-Z0-9_\-]*)$`, editBlog)
+	web.Post(`/(en/)?blogs/[a-zA-Z0-9_\-]*/comments$`, addBlogComment)
+	web.Post(`/login$`, login)
+
+	//API
+	web.Post(`/api/words/(2[0-9][1-9][0-9])-([0-1][0-9])-([0-3][0-9])$`, editWord)
+	web.Post(`/api/words$`, addWord)
+	web.Post(`/api/words/import$`, importWords)
+	web.Get(`/api/words/export$`, exportWords)
+
+	//Golang does not support mime well on windows. just for test
+	web.Get(`/(blogsoffline|offline).cfm`, serveCfm)
+
+	web.Run(fmt.Sprintf("0.0.0.0:%d", *port))
+}
+
+func serveCfm(ctx *web.Context, name string) {
+	all, err := ioutil.ReadFile(name + ".cfm")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	ctx.SetHeader("Content-Type", "text/cache-manifest", true)
+	ctx.Write(all)
+}
+
+func getParam(ctx *web.Context, name string, errMsgs []string, required bool) (string, []string) {
+	v, ok := ctx.Params[name]
+	if (!ok || strings.Trim(v, " ") == "") && required {
+		errMsgs = append(errMsgs, "Missing required parameter: "+name)
+	}
+	return v, errMsgs
+}
+
+func renderError(ctx *web.Context, code int, errMsgs []string) {
+	ctx.ResponseWriter.WriteHeader(code)
+	pages.Execute(ctx, "error", errMsgs)
+}
+
+func recompileTemplate() {
+	err := pages.Recompile()
+	if err != nil {
+		fmt.Println(err)
+	}
+}

server.log

-2011/12/19 16:37:00 web.go serving 0.0.0.0:8080
-2011/12/19 16:37:09 GET /
-2011/12/19 16:37:09 GET /blogs
-2011/12/19 16:37:09 GET /styles/bootstrap.min.css
-2011/12/19 16:37:09 GET /styles/mysite.css
-2011/12/19 16:37:09 GET /images/rss/google.gif
-2011/12/19 16:37:09 GET /images/rss/zhuaxia.gif
-2011/12/19 16:37:09 GET /images/rss/xianguo.gif
-2011/12/19 16:37:09 GET /images/rss/youdao.gif
-2011/12/19 16:37:09 GET /images/rss/msn.gif
-2011/12/19 16:37:09 GET /favicon.ico
-2011/12/19 16:37:09 GET /favicon.ico
-2011/12/19 16:37:09 GET /favicon.ico
-2011/12/19 16:37:11 GET /words
-2011/12/19 16:37:11 GET /login?url=/words
-2011/12/19 16:37:21 POST /login?url=/words
-2011/12/19 16:37:21 GET /words
-2011/12/19 16:37:21 GET /scripts/jquery.js
-2011/12/19 16:37:24 GET /words/2011-12-16
-2011/12/19 16:37:24 GET /images/stars.gif
-2011/12/19 16:37:26 GET /words
-2011/12/19 16:38:29 GET /words
-2011/12/19 16:38:37 POST /api/words
-2011/12/19 16:38:37 GET /words
-2011/12/19 16:38:39 GET /words/2011-12-19
-2011/12/20 17:23:24 web.go serving 0.0.0.0:8080
-2011/12/20 17:23:41 GET /words
-2011/12/20 17:23:41 GET /login?url=/words
-2011/12/20 17:23:41 GET /styles/mysite.css
-2011/12/20 17:23:49 POST /login?url=/words
-2011/12/20 17:23:49 GET /words
-2011/12/20 17:23:54 GET /words/2011-12-19
-2011/12/20 17:24:23 POST /api/words
-2011/12/20 17:24:23 GET /words
-2011/12/20 17:24:32 GET /words/2011-12-20
-2011/12/20 17:24:49 POST /api/words
-2011/12/20 17:24:49 GET /words/2011-12-20
-2011/12/20 17:25:21 POST /api/words
-2011/12/20 17:25:21 GET /words/2011-12-20
-2011/12/20 17:25:30 POST /api/words
-2011/12/20 17:25:30 GET /words/2011-12-20
-2011/12/27 11:55:52 web.go serving 0.0.0.0:8080
-2011/12/27 11:55:57 GET /
-2011/12/27 11:55:57 GET /blogs
-2011/12/27 11:55:57 GET /styles/bootstrap.min.css
-2011/12/27 11:55:57 GET /styles/mysite.css
-2011/12/27 11:55:57 GET /images/rss/google.gif
-2011/12/27 11:55:57 GET /images/rss/zhuaxia.gif
-2011/12/27 11:55:57 GET /images/rss/xianguo.gif
-2011/12/27 11:55:57 GET /images/rss/youdao.gif
-2011/12/27 11:55:57 GET /images/rss/msn.gif
-2011/12/27 11:55:57 GET /favicon.ico
-2011/12/27 11:57:03 GET /admin
-2011/12/27 11:57:03 GET /login?url=/admin
-2011/12/27 11:57:03 GET /favicon.ico
-2011/12/27 11:57:08 POST /login?url=/admin
-2011/12/27 11:57:08 GET /admin
-2011/12/27 11:57:08 GET /favicon.ico
-2011/12/27 11:57:10 GET /admin/refreshtpl
-2011/12/27 11:57:10 GET /admin
-2011/12/27 11:57:10 GET /favicon.ico
-2011/12/27 11:57:11 GET /words
-2011/12/27 11:57:11 GET /scripts/jquery.js
-2011/12/27 11:57:11 GET /favicon.ico
-2011/12/27 11:57:17 GET /favicon.ico
-2011/12/27 11:57:23 GET /words/2011-12-20
-2011/12/27 11:57:24 GET /offline.cfm
-2011/12/27 11:57:24 GET /script/jquery.js
-2011/12/27 11:57:24 GET /words/2011-12-20
-2011/12/27 11:57:24 GET /images/stars.gif
-2011/12/27 11:57:24 GET /favicon.ico
-2011/12/27 11:57:27 GET /words/2011-12-20
-2011/12/27 11:57:27 GET /favicon.ico
-2011/12/27 11:59:20 GET /words/2011-12-20
-2011/12/27 11:59:20 GET /offline.cfm
-2011/12/27 11:59:20 GET /script/jquery.js
-2011/12/27 11:59:20 GET /words/2011-12-20
-2011/12/27 11:59:20 GET /favicon.ico
-2011/12/27 12:00:12 GET /script/jquery.js
-2011/12/27 12:00:12 GET /favicon.ico
-2011/12/27 12:02:11 GET /words/2011-12-20
-2011/12/27 12:02:11 GET /styles/bootstrap.min.css
-2011/12/27 12:02:11 GET /styles/mysite.css
-2011/12/27 12:02:11 GET /offline.cfm
-2011/12/27 12:02:11 GET /words/2011-12-20
-2011/12/27 12:02:11 GET /scripts/jquery.js
-2011/12/27 12:02:11 GET /images/stars.gif
-2011/12/27 12:02:11 GET /offline.cfm
-2011/12/27 12:02:12 GET /favicon.ico
-2011/12/27 12:04:24 GET /admin
-2011/12/27 12:04:24 GET /favicon.ico
-2011/12/27 12:04:26 GET /admin/refreshtpl
-2011/12/27 12:04:26 GET /admin
-2011/12/27 12:04:26 GET /favicon.ico
-2011/12/27 12:04:27 GET /words
-2011/12/27 12:04:27 GET /offline.cfm
-2011/12/27 12:04:27 GET /words
-2011/12/27 12:04:27 GET /favicon.ico
-2011/12/27 12:07:50 GET /offline.cfm
-2011/12/27 12:07:50 GET /favicon.ico
-2011/12/27 12:15:52 POST /api/words
-2011/12/27 12:15:52 GET /offline.cfm
-2011/12/27 12:15:52 GET /favicon.ico
-2011/12/27 12:15:56 GET /offline.cfm
-2011/12/27 12:15:56 GET /favicon.ico
-2011/12/27 12:15:57 GET /offline.cfm
-2011/12/27 12:15:57 GET /favicon.ico
-2011/12/27 12:15:58 GET /offline.cfm
-2011/12/27 12:15:58 GET /favicon.ico
-2011/12/27 12:15:59 GET /words/2011-12-19
-2011/12/27 12:15:59 GET /offline.cfm
-2011/12/27 12:15:59 GET /words/2011-12-19
-2011/12/27 12:15:59 GET /favicon.ico
-2011/12/27 12:16:00 GET /offline.cfm
-2011/12/27 12:16:00 GET /favicon.ico
-2011/12/27 12:16:01 GET /offline.cfm
-2011/12/27 12:16:01 GET /favicon.ico
-2011/12/27 12:16:02 GET /offline.cfm
-2011/12/27 12:16:02 GET /favicon.ico
-2011/12/27 12:16:08 GET /offline.cfm
-2011/12/27 12:16:08 GET /favicon.ico
-2011/12/27 12:16:11 GET /offline.cfm
-2011/12/27 12:16:11 GET /favicon.ico
-2011/12/27 12:17:23 GET /offline.cfm
-2011/12/27 12:17:24 GET /favicon.ico
-2011/12/27 12:24:36 GET /offline.cfm
-2011/12/27 12:25:14 GET /offline.cfm
-2011/12/27 12:25:16 GET /favicon.ico
-2011/12/27 12:25:25 GET /offline.cfm
-2011/12/27 12:25:25 GET /favicon.ico
-2011/12/27 12:25:45 GET /offline.cfm
-2011/12/27 12:25:45 GET /words
-2011/12/27 12:25:45 GET /words/2011-12-19
-2011/12/27 12:25:45 GET /words/2011-12-20
-2011/12/27 12:25:45 GET /offline.cfm
-2011/12/27 12:25:45 GET /favicon.ico
-2011/12/27 12:26:24 GET /offline.cfm
-2011/12/27 12:26:34 GET /offline.cfm
-2011/12/27 12:26:34 GET /favicon.ico
-2011/12/27 12:26:43 GET /offline.cfm
-2011/12/27 12:26:43 GET /favicon.ico
-2011/12/27 13:22:40 GET /words/2011-12-27
-2011/12/27 13:22:40 GET /login?url=/words/2011-12-27
-2011/12/27 13:22:40 GET /favicon.ico
-2011/12/27 13:22:49 POST /login?url=/words/2011-12-27
-2011/12/27 13:22:49 GET /words/2011-12-27
-2011/12/27 13:22:49 GET /offline.cfm
-2011/12/27 13:22:49 GET /words/2011-12-27
-2011/12/27 13:22:50 GET /favicon.ico
-2011/12/27 13:23:09 POST /api/words
-2011/12/27 13:23:09 GET /offline.cfm
-2011/12/27 13:23:09 GET /favicon.ico
-2011/12/27 13:23:13 GET /offline.cfm
-2011/12/27 13:23:13 GET /favicon.ico
-2011/12/27 13:23:17 GET /offline.cfm
-2011/12/27 13:23:18 GET /favicon.ico
-2011/12/27 13:23:19 GET /offline.cfm
-2011/12/27 13:23:19 GET /favicon.ico
-2011/12/27 13:23:30 GET /offline.cfm
-2011/12/27 13:23:30 GET /words
-2011/12/27 13:23:30 GET /words/2011-12-19
-2011/12/27 13:23:30 GET /words/2011-12-27
-2011/12/27 13:23:30 GET /words/2011-12-20
-2011/12/27 13:23:30 GET /favicon.ico
-2011/12/27 13:23:30 GET /offline.cfm
-2011/12/27 13:23:45 GET /offline.cfm
-2011/12/27 13:23:45 GET /favicon.ico
-2011/12/27 13:24:17 GET /favicon.ico
-2011/12/27 13:24:22 GET /favicon.ico
-2011/12/27 13:24:40 GET /offline.cfm
-2011/12/27 13:24:40 GET /favicon.ico
-2011/12/27 13:24:53 GET /offline.cfm
-2011/12/27 13:24:53 GET /words
-2011/12/27 13:24:53 GET /words/2011-12-19
-2011/12/27 13:24:53 GET /words/2011-12-27
-2011/12/27 13:24:53 GET /words/2011-12-20
-2011/12/27 13:24:53 GET /offline.cfm
-2011/12/27 13:24:55 GET /favicon.ico
-2011/12/27 13:27:05 GET /offline.cfm
-2011/12/27 13:27:05 GET /words
-2011/12/27 13:27:05 GET /words/2011-12-20
-2011/12/27 13:27:05 GET /words/2011-12-27
-2011/12/27 13:27:05 GET /words/2011-12-19
-2011/12/27 13:27:05 GET /favicon.ico
-2011/12/27 13:27:05 GET /offline.cfm
-2011/12/27 13:27:16 GET /offline.cfm
-2011/12/27 13:27:26 GET /offline.cfm
-2011/12/27 13:27:27 GET /favicon.ico
-2011/12/27 13:28:19 GET /offline.cfm
-2011/12/27 13:28:19 GET /words
-2011/12/27 13:28:19 GET /words/2011-12-19
-2011/12/27 13:28:19 GET /words/2011-12-27
-2011/12/27 13:28:19 GET /words/2011-12-20
-2011/12/27 13:28:19 GET /offline.cfm
-2011/12/27 13:28:20 GET /favicon.ico
-2011/12/27 13:28:34 GET /offline.cfm
-2011/12/27 13:28:34 GET /favicon.ico
-2011/12/27 13:29:16 GET /favicon.ico
-2011/12/27 13:29:27 GET /favicon.ico
-2011/12/27 14:28:05 GET /offline.cfm
-2011/12/27 14:28:05 GET /favicon.ico
-2011/12/27 14:28:16 GET /offline.cfm
-2011/12/27 14:28:16 GET /words/2011-12-19
-2011/12/27 14:28:16 GET /words
-2011/12/27 14:28:16 GET /words/2011-12-27
-2011/12/27 14:28:16 GET /words/2011-12-20
-2011/12/27 14:28:16 GET /offline.cfm
-2011/12/27 14:28:16 GET /favicon.ico
-2011/12/27 14:28:42 GET /favicon.ico
-2011/12/27 14:28:43 GET /favicon.ico
-2011/12/27 14:35:02 GET /admin
-2011/12/27 14:35:02 GET /login?url=/admin
-2011/12/27 14:35:02 GET /styles/bootstrap.min.css
-2011/12/27 14:35:02 GET /styles/mysite.css
-2011/12/27 14:35:08 POST /login?url=/admin
-2011/12/27 14:35:08 GET /admin
-2011/12/27 14:35:10 GET /words
-2011/12/27 14:35:10 GET /scripts/jquery.js
-2011/12/27 14:35:16 GET /offline.cfm
-2011/12/27 14:35:17 GET /words