diff --git a/template.go b/template.go index 9a1cfe93f..40c1490a9 100644 --- a/template.go +++ b/template.go @@ -15,6 +15,8 @@ import ( "regexp" "strconv" "strings" + "sync" + "sync/atomic" ) // ErrorCSSClass httml CSS error class name @@ -23,16 +25,14 @@ 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 { - // Template data and implementation - templatesAndEngineList []TemplateEngine - // If an error was encountered parsing the templates, it is stored here. - compileError *Error // Paths to search for templates, in priority order. paths []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 + // 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 { @@ -56,32 +56,61 @@ func NewTemplateLoader(paths []string) *TemplateLoader { return loader } -// 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) +// 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{}} + TRACE.Printf("Refreshing templates from %s", loader.paths) - if len(loader.templatesAndEngineList) == 0 { - if err = loader.InitializeEngines(GO_TEMPLATE); err != nil { - return - } + if err = loader.initializeEngines(runtimeLoader, GO_TEMPLATE); err != nil { + return } - for _, engine := range loader.templatesAndEngineList { + for _, engine := range runtimeLoader.templatesAndEngineList { engine.Event(TEMPLATE_REFRESH_REQUESTED, nil) } fireEvent(TEMPLATE_REFRESH_REQUESTED, nil) defer func() { - for _, engine := range loader.templatesAndEngineList { + for _, engine := range runtimeLoader.templatesAndEngineList { engine.Event(TEMPLATE_REFRESH_COMPLETED, nil) } fireEvent(TEMPLATE_REFRESH_COMPLETED, nil) - // Reset the TemplateMap, we don't prepopulate the map because - loader.TemplateMap = map[string]Template{} + // 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") @@ -94,8 +123,8 @@ func (loader *TemplateLoader) Refresh() (err *Error) { } TRACE.Printf("Refreshing templates from %s", loader.paths) - loader.compileError = nil - loader.TemplatePaths = map[string]string{} + 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 @@ -133,14 +162,14 @@ func (loader *TemplateLoader) Refresh() (err *Error) { return nil } - fileBytes, err := loader.findAndAddTemplate(path, fullSrcDir, basePath) + fileBytes, err := runtimeLoader.findAndAddTemplate(path, fullSrcDir, basePath) // Store / report the first error encountered. - if err != nil && loader.compileError == nil { - loader.compileError, _ = err.(*Error) - if nil == loader.compileError { + if err != nil && runtimeLoader.compileError == nil { + runtimeLoader.compileError, _ = err.(*Error) + if nil == runtimeLoader.compileError { _, line, description := ParseTemplateError(err) - loader.compileError = &Error{ + runtimeLoader.compileError = &Error{ Title: "Template Compilation Error", Path: path, Description: description, @@ -149,7 +178,7 @@ func (loader *TemplateLoader) Refresh() (err *Error) { } } ERROR.Printf("Template compilation error (In %s around line %d):\n\t%s", - path, loader.compileError.Line, err.Error()) + path, runtimeLoader.compileError.Line, err.Error()) } else if nil != err { //&& strings.HasPrefix(templateName, "errors/") { if compileError, ok := err.(*Error); ok { @@ -173,18 +202,32 @@ func (loader *TemplateLoader) Refresh() (err *Error) { // If there was an error with the Funcs, set it and return immediately. if funcErr != nil { - loader.compileError = funcErr.(*Error) - return loader.compileError + runtimeLoader.compileError = funcErr.(*Error) + return runtimeLoader.compileError } } // Note: compileError may or may not be set. - return loader.compileError + 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 + // 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 (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath string) (fileBytes []byte, err error) { +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 == '\\' { @@ -192,7 +235,7 @@ func (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath stri } // Check to see if template was found - if place, found := loader.TemplatePaths[templateName]; found { + if place, found := runtimeLoader.TemplatePaths[templateName]; found { TRACE.Println("Not Loading, template is already exists: ", templateName, "\r\n\told file:", place, "\r\n\tnew file:", path) return @@ -205,7 +248,7 @@ func (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath stri } // Parse template file and replace the "_RNS_|" in the template with the module name // allow for namespaces to be renamed "_RNS_(.*?)|" - if module := ModuleFromPath(path, false);module != nil { + if module := ModuleFromPath(path, false); module != nil { fileBytes = namespaceReplace(fileBytes, module) } @@ -213,17 +256,17 @@ func (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath stri baseTemplate := NewBaseTemplate(templateName, path, basePath, fileBytes) // Try to find a default engine for the file - for _, engine := range loader.templatesAndEngineList { + for _, engine := range runtimeLoader.templatesAndEngineList { if engine.Handles(baseTemplate) { - _, err = loader.loadIntoEngine(engine, baseTemplate) + _, err = runtimeLoader.loadIntoEngine(engine, baseTemplate) return } } // Try all engines available var defaultError error - for _, engine := range loader.templatesAndEngineList { - if loaded, loaderr := loader.loadIntoEngine(engine, baseTemplate); loaded { + for _, engine := range runtimeLoader.templatesAndEngineList { + if loaded, loaderr := runtimeLoader.loadIntoEngine(engine, baseTemplate); loaded { return } else { TRACE.Printf("Engine '%s' unable to compile %s %s", engine.Name(), path, loaderr) @@ -244,7 +287,14 @@ func (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath stri return } -func (loader *TemplateLoader) loadIntoEngine(engine TemplateEngine, baseTemplate *TemplateView) (loaded bool, err error) { +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 + TRACE.Println("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 TRACE.Println("template already exists: ", baseTemplate.TemplateName, " in engine ", engine.Name(), "\r\n\told file:", @@ -253,7 +303,10 @@ func (loader *TemplateLoader) loadIntoEngine(engine TemplateEngine, baseTemplate return } if err = engine.ParseAndAdd(baseTemplate); err == nil { - loader.TemplatePaths[baseTemplate.TemplateName] = baseTemplate.FilePath + if tmpl := engine.Lookup(baseTemplate.TemplateName); tmpl != nil { + runtimeLoader.templateMap[baseTemplate.TemplateName] = tmpl + } + runtimeLoader.TemplatePaths[baseTemplate.TemplateName] = baseTemplate.FilePath TRACE.Printf("Engine '%s' compiled %s", engine.Name(), baseTemplate.FilePath) loaded = true } else { @@ -262,20 +315,6 @@ func (loader *TemplateLoader) loadIntoEngine(engine TemplateEngine, baseTemplate return } -// 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, ".") -} - // 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) { @@ -300,53 +339,94 @@ func ParseTemplateError(err error) (templateName string, line int, description s return templateName, line, description } -// DEPRECATED Use TemplateLang, will be removed in future release -func (loader *TemplateLoader) Template(name string) (tmpl Template, err error) { - return loader.TemplateLang(name, "") -} // 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 (loader *TemplateLoader) TemplateLang(name, lang string) (tmpl Template, err error) { - if loader.compileError != nil { - return nil, loader.compileError - } -// Attempt to load a localized template first. - if lang != "" { - // Look up and return the template. - tmpl = loader.templateLoad(name + "." + lang) - } - // Return non localized version - if tmpl == nil { - tmpl = loader.templateLoad(name) +func (runtimeLoader *templateRuntime) TemplateLang(name, lang string) (tmpl Template, err error) { + if runtimeLoader.compileError != nil { + return nil, runtimeLoader.compileError } - if tmpl == nil && err == nil { + // Fetch the template from the map + tmpl = runtimeLoader.templateLoad(name, lang) + if tmpl == nil { err = fmt.Errorf("Template %s not found.", name) } return } -func (loader *TemplateLoader) templateLoad(name string) (tmpl Template) { - if t,found := loader.TemplateMap[name];!found && t != nil { - tmpl = t + +// 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 loader.templatesAndEngineList { + for _, engine := range runtimeLoader.templatesAndEngineList { + if tmpl = engine.Lookup(langName); tmpl != nil { + found = true + break + } if tmpl = engine.Lookup(name); tmpl != nil { - loader.TemplateMap[name] = tmpl + 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 @@ -358,7 +438,7 @@ func (i *TemplateView) Content() (content []string) { } return nil } + func NewBaseTemplate(templateName, filePath, basePath string, fileBytes []byte) *TemplateView { return &TemplateView{TemplateName: templateName, FilePath: filePath, FileBytes: fileBytes, BasePath: basePath} } - diff --git a/template_engine.go b/template_engine.go index acebfbcd7..077a387a3 100644 --- a/template_engine.go +++ b/template_engine.go @@ -67,25 +67,25 @@ func (loader *TemplateLoader) CreateTemplateEngine(templateEngineName string) (T } // Passing in a comma delimited list of engine names to be used with this loader to parse the template files -func (loader *TemplateLoader) InitializeEngines(templateEngineNameList string) (err *Error) { +func (loader *TemplateLoader) initializeEngines(runtimeLoader *templateRuntime, templateEngineNameList string) (err *Error) { // Walk through the template loader's paths and build up a template set. if templateEngineNameList == "" { templateEngineNameList = GO_TEMPLATE } - loader.templatesAndEngineList = []TemplateEngine{} + runtimeLoader.templatesAndEngineList = []TemplateEngine{} for _, engine := range strings.Split(templateEngineNameList, ",") { engine := strings.TrimSpace(strings.ToLower(engine)) if templateLoader, err := loader.CreateTemplateEngine(engine); err != nil { - loader.compileError = &Error{ + runtimeLoader.compileError = &Error{ Title: "Panic (Template Loader)", Description: err.Error(), } - return loader.compileError + return runtimeLoader.compileError } else { // Always assign a default engine, switch it if it is specified in the config - loader.templatesAndEngineList = append(loader.templatesAndEngineList, templateLoader) + runtimeLoader.templatesAndEngineList = append(runtimeLoader.templatesAndEngineList, templateLoader) } } return