Add API Framework Revel Source Files
[iec.git] / src / foundation / api / revel / template.go
diff --git a/src/foundation/api/revel/template.go b/src/foundation/api/revel/template.go
new file mode 100644 (file)
index 0000000..c5bd91e
--- /dev/null
@@ -0,0 +1,477 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "bufio"
+       "bytes"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "path/filepath"
+       "regexp"
+       "strconv"
+       "strings"
+       "sync"
+       "sync/atomic"
+)
+
+// ErrorCSSClass httml CSS error class name
+var ErrorCSSClass = "hasError"
+
+// TemplateLoader object handles loading and parsing of templates.
+// Everything below the application's views directory is treated as a template.
+type TemplateLoader struct {
+       // Paths to search for templates, in priority order.
+       paths []string
+       // load version seed for templates
+       loadVersionSeed int
+       // A templateRuntime of looked up template results
+       runtimeLoader atomic.Value
+       // Lock to prevent concurrent map writes
+       templateMutex sync.Mutex
+}
+
+type Template interface {
+       // The name of the template.
+       Name() string // Name of template
+       // The content of the template as a string (Used in error handling).
+       Content() []string // Content
+       // Called by the server to render the template out the io.Writer, context contains the view args to be passed to the template.
+       Render(wr io.Writer, context interface{}) error
+       // The full path to the file on the disk.
+       Location() string // Disk location
+}
+
+var invalidSlugPattern = regexp.MustCompile(`[^a-z0-9 _-]`)
+var whiteSpacePattern = regexp.MustCompile(`\s+`)
+var templateLog = RevelLog.New("section", "template")
+
+// TemplateOutputArgs returns the result of the template rendered using the passed in arguments.
+func TemplateOutputArgs(templatePath string, args map[string]interface{}) (data []byte, err error) {
+       // Get the Template.
+       lang, _ := args[CurrentLocaleViewArg].(string)
+       template, err := MainTemplateLoader.TemplateLang(templatePath, lang)
+       if err != nil {
+               return nil, err
+       }
+       tr := &RenderTemplateResult{
+               Template: template,
+               ViewArgs: args,
+       }
+       b, err := tr.ToBytes()
+       if err != nil {
+               return nil, err
+       }
+       return b.Bytes(), nil
+}
+
+func NewTemplateLoader(paths []string) *TemplateLoader {
+       loader := &TemplateLoader{
+               paths: paths,
+       }
+       return loader
+}
+
+// WatchDir returns true of directory doesn't start with . (dot)
+// otherwise false
+func (loader *TemplateLoader) WatchDir(info os.FileInfo) bool {
+       // Watch all directories, except the ones starting with a dot.
+       return !strings.HasPrefix(info.Name(), ".")
+}
+
+// WatchFile returns true of file doesn't start with . (dot)
+// otherwise false
+func (loader *TemplateLoader) WatchFile(basename string) bool {
+       // Watch all files, except the ones starting with a dot.
+       return !strings.HasPrefix(basename, ".")
+}
+
+// DEPRECATED Use TemplateLang, will be removed in future release
+func (loader *TemplateLoader) Template(name string) (tmpl Template, err error) {
+       runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime)
+       return runtimeLoader.TemplateLang(name, "")
+}
+
+func (loader *TemplateLoader) TemplateLang(name, lang string) (tmpl Template, err error) {
+       runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime)
+       return runtimeLoader.TemplateLang(name, lang)
+}
+
+// Refresh method scans the views directory and parses all templates as Go Templates.
+// If a template fails to parse, the error is set on the loader.
+// (It's awkward to refresh a single Go Template)
+func (loader *TemplateLoader) Refresh() (err *Error) {
+       loader.templateMutex.Lock()
+       defer loader.templateMutex.Unlock()
+
+       loader.loadVersionSeed++
+       runtimeLoader := &templateRuntime{loader: loader,
+               version:     loader.loadVersionSeed,
+               templateMap: map[string]Template{}}
+
+       templateLog.Debug("Refresh: Refreshing templates from ", "path", loader.paths)
+       if err = loader.initializeEngines(runtimeLoader, Config.StringDefault("template.engines", GO_TEMPLATE)); err != nil {
+               return
+       }
+       for _, engine := range runtimeLoader.templatesAndEngineList {
+               engine.Event(TEMPLATE_REFRESH_REQUESTED, nil)
+       }
+       RaiseEvent(TEMPLATE_REFRESH_REQUESTED, nil)
+       defer func() {
+               for _, engine := range runtimeLoader.templatesAndEngineList {
+                       engine.Event(TEMPLATE_REFRESH_COMPLETED, nil)
+               }
+               RaiseEvent(TEMPLATE_REFRESH_COMPLETED, nil)
+
+               // Reset the runtimeLoader
+               loader.runtimeLoader.Store(runtimeLoader)
+       }()
+
+       // Resort the paths, make sure the revel path is the last path,
+       // so anything can override it
+       revelTemplatePath := filepath.Join(RevelPath, "templates")
+       // Go through the paths
+       for i, o := range loader.paths {
+               if o == revelTemplatePath && i != len(loader.paths)-1 {
+                       loader.paths[i] = loader.paths[len(loader.paths)-1]
+                       loader.paths[len(loader.paths)-1] = revelTemplatePath
+               }
+       }
+       templateLog.Debug("Refresh: Refreshing templates from", "path", loader.paths)
+
+       runtimeLoader.compileError = nil
+       runtimeLoader.TemplatePaths = map[string]string{}
+
+       for _, basePath := range loader.paths {
+               // Walk only returns an error if the template loader is completely unusable
+               // (namely, if one of the TemplateFuncs does not have an acceptable signature).
+
+               // Handling symlinked directories
+               var fullSrcDir string
+               f, err := os.Lstat(basePath)
+               if err == nil && f.Mode()&os.ModeSymlink == os.ModeSymlink {
+                       fullSrcDir, err = filepath.EvalSymlinks(basePath)
+                       if err != nil {
+                               templateLog.Panic("Refresh: Eval symlinks error ", "error", err)
+                       }
+               } else {
+                       fullSrcDir = basePath
+               }
+
+               var templateWalker filepath.WalkFunc
+
+               templateWalker = func(path string, info os.FileInfo, err error) error {
+                       if err != nil {
+                               templateLog.Error("Refresh: error walking templates:", "error", err)
+                               return nil
+                       }
+
+                       // Walk into watchable directories
+                       if info.IsDir() {
+                               if !loader.WatchDir(info) {
+                                       return filepath.SkipDir
+                               }
+                               return nil
+                       }
+
+                       // Only add watchable
+                       if !loader.WatchFile(info.Name()) {
+                               return nil
+                       }
+
+                       fileBytes, err := runtimeLoader.findAndAddTemplate(path, fullSrcDir, basePath)
+                       if err != nil {
+                               // Add in this template name to the list of templates unable to be compiled
+                               runtimeLoader.compileErrorNameList = append(runtimeLoader.compileErrorNameList, filepath.ToSlash(path[len(fullSrcDir)+1:]))
+                       }
+                       // Store / report the first error encountered.
+                       if err != nil && runtimeLoader.compileError == nil {
+                               runtimeLoader.compileError, _ = err.(*Error)
+
+                               if nil == runtimeLoader.compileError {
+                                       _, line, description := ParseTemplateError(err)
+
+                                       runtimeLoader.compileError = &Error{
+                                               Title:       "Template Compilation Error",
+                                               Path:        path,
+                                               Description: description,
+                                               Line:        line,
+                                               SourceLines: strings.Split(string(fileBytes), "\n"),
+                                       }
+                               }
+                               templateLog.Errorf("Refresh: Template compilation error (In %s around line %d):\n\t%s",
+                                       path, runtimeLoader.compileError.Line, err.Error())
+                       } else if nil != err { //&& strings.HasPrefix(templateName, "errors/") {
+
+                               if compileError, ok := err.(*Error); ok {
+                                       templateLog.Errorf("Template compilation error (In %s around line %d):\n\t%s",
+                                               path, compileError.Line, err.Error())
+                               } else {
+                                       templateLog.Errorf("Template compilation error (In %s ):\n\t%s",
+                                               path, err.Error())
+                               }
+                       }
+                       return nil
+               }
+
+               if _, err = os.Lstat(fullSrcDir); os.IsNotExist(err) {
+                       // #1058 Given views/template path is not exists
+                       // so no need to walk, move on to next path
+                       continue
+               }
+
+               funcErr := Walk(fullSrcDir, templateWalker)
+
+               // If there was an error with the Funcs, set it and return immediately.
+               if funcErr != nil {
+                       runtimeLoader.compileError = NewErrorFromPanic(funcErr)
+                       return runtimeLoader.compileError
+               }
+       }
+
+       // Note: compileError may or may not be set.
+       return runtimeLoader.compileError
+}
+
+type templateRuntime struct {
+       loader *TemplateLoader
+       // load version for templates
+       version int
+       // Template data and implementation
+       templatesAndEngineList []TemplateEngine
+       // If an error was encountered parsing the templates, it is stored here.
+       compileError *Error
+       // A list of the names of the templates with errors
+       compileErrorNameList []string
+       // Map from template name to the path from whence it was loaded.
+       TemplatePaths map[string]string
+       // A map of looked up template results
+       templateMap map[string]Template
+}
+
+// Checks to see if template exists in templatePaths, if so it is skipped (templates are imported in order
+// reads the template file into memory, replaces namespace keys with module (if found
+func (runtimeLoader *templateRuntime) findAndAddTemplate(path, fullSrcDir, basePath string) (fileBytes []byte, err error) {
+       templateName := filepath.ToSlash(path[len(fullSrcDir)+1:])
+       // Convert template names to use forward slashes, even on Windows.
+       if os.PathSeparator == '\\' {
+               templateName = strings.Replace(templateName, `\`, `/`, -1) // `
+       }
+
+       // Check to see if template was found
+       if place, found := runtimeLoader.TemplatePaths[templateName]; found {
+               templateLog.Debug("findAndAddTemplate: Not Loading, template is already exists: ", "name", templateName, "old",
+                       place, "new", path)
+               return
+       }
+
+       fileBytes, err = ioutil.ReadFile(path)
+       if err != nil {
+               templateLog.Error("findAndAddTemplate: Failed reading file:", "path", path, "error", err)
+               return
+       }
+       // Parse template file and replace the "_LOCAL_|" in the template with the module name
+       // allow for namespaces to be renamed "_LOCAL_(.*?)|"
+       if module := ModuleFromPath(path, false); module != nil {
+               fileBytes = namespaceReplace(fileBytes, module)
+       }
+
+       // if we have an engine picked for this template process it now
+       baseTemplate := NewBaseTemplate(templateName, path, basePath, fileBytes)
+
+       // Try to find a default engine for the file
+       for _, engine := range runtimeLoader.templatesAndEngineList {
+               if engine.Handles(baseTemplate) {
+                       _, err = runtimeLoader.loadIntoEngine(engine, baseTemplate)
+                       return
+               }
+       }
+
+       // Try all engines available
+       var defaultError error
+       for _, engine := range runtimeLoader.templatesAndEngineList {
+               if loaded, loaderr := runtimeLoader.loadIntoEngine(engine, baseTemplate); loaded {
+                       return
+               } else {
+                       templateLog.Debugf("findAndAddTemplate: Engine '%s' unable to compile %s %s", engine.Name(), path, loaderr.Error())
+                       if defaultError == nil {
+                               defaultError = loaderr
+                       }
+               }
+       }
+
+       // Assign the error from the first parser
+       err = defaultError
+
+       // No engines could be found return the err
+       if err == nil {
+               err = fmt.Errorf("Failed to parse template file using engines %s", path)
+       }
+
+       return
+}
+
+func (runtimeLoader *templateRuntime) loadIntoEngine(engine TemplateEngine, baseTemplate *TemplateView) (loaded bool, err error) {
+       if loadedTemplate, found := runtimeLoader.templateMap[baseTemplate.TemplateName]; found {
+               // Duplicate template found in map
+               templateLog.Debug("template already exists in map: ", baseTemplate.TemplateName, " in engine ", engine.Name(), "\r\n\told file:",
+                       loadedTemplate.Location(), "\r\n\tnew file:", baseTemplate.FilePath)
+               return
+       }
+
+       if loadedTemplate := engine.Lookup(baseTemplate.TemplateName); loadedTemplate != nil {
+               // Duplicate template found for engine
+               templateLog.Debug("loadIntoEngine: template already exists: ", "template", baseTemplate.TemplateName, "inengine ", engine.Name(), "old",
+                       loadedTemplate.Location(), "new", baseTemplate.FilePath)
+               loaded = true
+               return
+       }
+       if err = engine.ParseAndAdd(baseTemplate); err == nil {
+               if tmpl := engine.Lookup(baseTemplate.TemplateName); tmpl != nil {
+                       runtimeLoader.templateMap[baseTemplate.TemplateName] = tmpl
+               }
+               runtimeLoader.TemplatePaths[baseTemplate.TemplateName] = baseTemplate.FilePath
+               templateLog.Debugf("loadIntoEngine:Engine '%s' compiled %s", engine.Name(), baseTemplate.FilePath)
+               loaded = true
+       } else {
+               templateLog.Debug("loadIntoEngine: Engine failed to compile", "engine", engine.Name(), "file", baseTemplate.FilePath, "error", err)
+       }
+       return
+}
+
+// Parse the line, and description from an error message like:
+// html/template:Application/Register.html:36: no such template "footer.html"
+func ParseTemplateError(err error) (templateName string, line int, description string) {
+       if e, ok := err.(*Error); ok {
+               return "", e.Line, e.Description
+       }
+
+       description = err.Error()
+       i := regexp.MustCompile(`:\d+:`).FindStringIndex(description)
+       if i != nil {
+               line, err = strconv.Atoi(description[i[0]+1 : i[1]-1])
+               if err != nil {
+                       templateLog.Error("ParseTemplateError: Failed to parse line number from error message:", "error", err)
+               }
+               templateName = description[:i[0]]
+               if colon := strings.Index(templateName, ":"); colon != -1 {
+                       templateName = templateName[colon+1:]
+               }
+               templateName = strings.TrimSpace(templateName)
+               description = description[i[1]+1:]
+       }
+       return templateName, line, description
+}
+
+// Template returns the Template with the given name.  The name is the template's path
+// relative to a template loader root.
+//
+// An Error is returned if there was any problem with any of the templates.  (In
+// this case, if a template is returned, it may still be usable.)
+func (runtimeLoader *templateRuntime) TemplateLang(name, lang string) (tmpl Template, err error) {
+       if runtimeLoader.compileError != nil {
+               for _, errName := range runtimeLoader.compileErrorNameList {
+                       if name == errName {
+                               return nil, runtimeLoader.compileError
+                       }
+               }
+       }
+
+       // Fetch the template from the map
+       tmpl = runtimeLoader.templateLoad(name, lang)
+       if tmpl == nil {
+               err = fmt.Errorf("Template %s not found.", name)
+       }
+
+       return
+}
+
+// Load and also updates map if name is not found (to speed up next lookup)
+func (runtimeLoader *templateRuntime) templateLoad(name, lang string) (tmpl Template) {
+       langName := name
+       found := false
+       if lang != "" {
+               // Look up and return the template.
+               langName = name + "." + lang
+               tmpl, found = runtimeLoader.templateMap[langName]
+               if found {
+                       return
+               }
+               tmpl, found = runtimeLoader.templateMap[name]
+       } else {
+               tmpl, found = runtimeLoader.templateMap[name]
+               if found {
+                       return
+               }
+       }
+
+       if !found {
+               // Neither name is found
+               // Look up and return the template.
+               for _, engine := range runtimeLoader.templatesAndEngineList {
+                       if tmpl = engine.Lookup(langName); tmpl != nil {
+                               found = true
+                               break
+                       }
+                       if tmpl = engine.Lookup(name); tmpl != nil {
+                               found = true
+                               break
+                       }
+               }
+               if !found {
+                       return
+               }
+       }
+
+       // If we found anything store it in the map, we need to copy so we do not
+       // run into concurrency issues
+       runtimeLoader.loader.templateMutex.Lock()
+       defer runtimeLoader.loader.templateMutex.Unlock()
+
+       // In case another thread has loaded the map, reload the atomic value and check
+       newRuntimeLoader := runtimeLoader.loader.runtimeLoader.Load().(*templateRuntime)
+       if newRuntimeLoader.version != runtimeLoader.version {
+               return
+       }
+
+       newTemplateMap := map[string]Template{}
+       for k, v := range newRuntimeLoader.templateMap {
+               newTemplateMap[k] = v
+       }
+       newTemplateMap[langName] = tmpl
+       if _, found := newTemplateMap[name]; !found {
+               newTemplateMap[name] = tmpl
+       }
+       runtimeCopy := &templateRuntime{}
+       *runtimeCopy = *newRuntimeLoader
+       runtimeCopy.templateMap = newTemplateMap
+
+       // Set the atomic value
+       runtimeLoader.loader.runtimeLoader.Store(runtimeCopy)
+       return
+}
+
+func (i *TemplateView) Location() string {
+       return i.FilePath
+}
+
+func (i *TemplateView) Content() (content []string) {
+       if i.FileBytes != nil {
+               // Parse the bytes
+               buffer := bytes.NewBuffer(i.FileBytes)
+               reader := bufio.NewScanner(buffer)
+               for reader.Scan() {
+                       content = append(content, string(reader.Bytes()))
+               }
+       }
+
+       return content
+}
+
+func NewBaseTemplate(templateName, filePath, basePath string, fileBytes []byte) *TemplateView {
+       return &TemplateView{TemplateName: templateName, FilePath: filePath, FileBytes: fileBytes, BasePath: basePath}
+}