github.com/scottcagno/storage@v1.8.0/pkg/web/template.go (about) 1 package web 2 3 import ( 4 "errors" 5 "fmt" 6 "github.com/scottcagno/storage/pkg/web/logging" 7 "html/template" 8 "io" 9 "log" 10 "mime" 11 "net/http" 12 "os" 13 "path/filepath" 14 "sync" 15 ) 16 17 var ( 18 defaultTemplatePattern = "web/templates/*.html" 19 defaultStubsPattern = "web/templates/*/*.html" 20 ) 21 22 type TemplateConfig struct { 23 StubsPattern string 24 TemplatePattern string 25 StdErrLogger *log.Logger 26 FuncMap template.FuncMap 27 } 28 29 func checkTemplateConfig(conf *TemplateConfig) *TemplateConfig { 30 if conf == nil { 31 conf = &TemplateConfig{ 32 TemplatePattern: defaultTemplatePattern, 33 StdErrLogger: logging.NewStdErrLogger(os.Stderr), 34 FuncMap: template.FuncMap{}, 35 } 36 } 37 if conf.TemplatePattern == *new(string) { 38 conf.TemplatePattern = defaultTemplatePattern 39 } 40 if conf.StdErrLogger == nil { 41 conf.StdErrLogger = logging.NewStdErrLogger(os.Stderr) 42 } 43 return conf 44 } 45 46 // TemplateCache is a template engine that caches golang html/template files 47 type TemplateCache struct { 48 conf *TemplateConfig 49 cache *template.Template 50 } 51 52 // NewTemplateCache takes a pattern to glob and an optional logger and returns a 53 // new *TemplateCache instance. On success, it returns a nil error. An example 54 // pattern to glob would be: "web/templates/*.html" or "my-path/*.tmpl.html" 55 func NewTemplateCache(conf *TemplateConfig) (*TemplateCache, error) { 56 sconf := checkTemplateConfig(conf) 57 t, err := template.New("*").Funcs(conf.FuncMap).ParseGlob(conf.TemplatePattern) 58 if err != nil { 59 return nil, err 60 } 61 tc := &TemplateCache{ 62 cache: t, 63 conf: sconf, 64 } 65 return tc, nil 66 } 67 68 func NewTemplateCacheWithSeparateStubs(conf *TemplateConfig) (*TemplateCache, error) { 69 sconf := checkTemplateConfig(conf) 70 t, err := template.New("*").Funcs(conf.FuncMap).ParseGlob(conf.TemplatePattern) 71 if err != nil { 72 return nil, err 73 } 74 if matches, _ := filepath.Glob(conf.StubsPattern); len(matches) > 0 { 75 t, err = t.ParseGlob(conf.StubsPattern) 76 if err != nil { 77 return nil, err 78 } 79 } 80 tc := &TemplateCache{ 81 cache: t, 82 conf: sconf, 83 } 84 return tc, nil 85 } 86 87 func NewTemplateCacheWithFiles(conf *TemplateConfig, files ...string) (*TemplateCache, error) { 88 sconf := checkTemplateConfig(conf) 89 t, err := template.New("*").Funcs(sconf.FuncMap).ParseFiles(files...) 90 if err != nil { 91 return nil, err 92 } 93 tc := &TemplateCache{ 94 cache: t, 95 conf: sconf, 96 } 97 return tc, nil 98 } 99 100 func (t *TemplateCache) AddSeparateStubs(stubsPattern string) error { 101 var err error 102 if matches, _ := filepath.Glob(stubsPattern); len(matches) > 0 { 103 t.cache, err = t.cache.ParseGlob(stubsPattern) 104 if err != nil { 105 return err 106 } 107 } 108 return nil 109 } 110 111 func (t *TemplateCache) Templates() ([]*template.Template, string) { 112 return t.cache.Templates(), t.cache.DefinedTemplates() 113 } 114 115 func (t *TemplateCache) Use(name string) *template.Template { 116 return t.cache.Lookup(name) 117 } 118 119 func (t *TemplateCache) Lookup(name string) *template.Template { 120 return t.cache.Lookup(name) 121 } 122 123 func (t *TemplateCache) ExecuteTemplate(w io.Writer, name string, data interface{}) error { 124 err := t.cache.ExecuteTemplate(w, name, data) 125 if err != nil { 126 return err 127 } 128 return nil 129 } 130 131 func (t *TemplateCache) RenderWithBuffer(w http.ResponseWriter, r *http.Request, tmpl string, data interface{}) { 132 bufPool := OpenBufferPool() 133 buffer := bufPool.Get() 134 err := t.cache.ExecuteTemplate(buffer, tmpl, data) 135 if err != nil { 136 bufPool.Put(buffer) 137 t.conf.StdErrLogger.Printf("Error while executing template (%s): %v\n", tmpl, err) 138 http.Redirect(w, r, "/error/404", http.StatusTemporaryRedirect) 139 return 140 } 141 _, err = buffer.WriteTo(w) 142 if err != nil { 143 t.conf.StdErrLogger.Printf("Error while writing (Render) to ResponseWriter: %v\n", err) 144 } 145 bufPool.Put(buffer) 146 return 147 } 148 149 func (t *TemplateCache) Render(w http.ResponseWriter, r *http.Request, tmpl string, data interface{}) { 150 err := t.cache.ExecuteTemplate(w, tmpl, data) 151 if err != nil { 152 t.conf.StdErrLogger.Printf("Error while executing template (%s): %v\n", tmpl, err) 153 http.Redirect(w, r, "/error/404", http.StatusTemporaryRedirect) 154 return 155 } 156 if err != nil { 157 t.conf.StdErrLogger.Printf("Error while writing (Render) to ResponseWriter: %v\n", err) 158 } 159 return 160 } 161 162 func (t *TemplateCache) Raw(w http.ResponseWriter, format string, data ...interface{}) { 163 _, err := fmt.Fprintf(w, format, data...) 164 if err != nil { 165 t.conf.StdErrLogger.Printf("Error while writing (Raw) to ResponseWriter: %v\n", err) 166 return 167 } 168 return 169 } 170 171 func (t *TemplateCache) ContentType(w http.ResponseWriter, content string) { 172 ct := mime.TypeByExtension(content) 173 if ct == "" { 174 t.conf.StdErrLogger.Printf("Error, incompatible content type!\n") 175 return 176 } 177 w.Header().Set("Content-Type", ct) 178 return 179 } 180 181 func (t *TemplateCache) Handle(name string) http.Handler { 182 tmpl := t.Lookup(name) 183 if tmpl == nil { 184 return http.NotFoundHandler() 185 } 186 fn := func(w http.ResponseWriter, r *http.Request) { 187 err := tmpl.Execute(w, nil) 188 if err != nil { 189 code := http.StatusInternalServerError 190 http.Error(w, http.StatusText(code), code) 191 return 192 } 193 return 194 } 195 return http.HandlerFunc(fn) 196 } 197 198 func (t *TemplateCache) HandleWithData(name string, data interface{}) http.Handler { 199 tmpl := t.Lookup(name) 200 if tmpl == nil { 201 return http.NotFoundHandler() 202 } 203 fn := func(w http.ResponseWriter, r *http.Request) { 204 err := tmpl.Execute(w, data) 205 if err != nil { 206 code := http.StatusInternalServerError 207 http.Error(w, http.StatusText(code), code) 208 return 209 } 210 return 211 } 212 return http.HandlerFunc(fn) 213 } 214 215 type TemplateManager struct { 216 lock sync.RWMutex 217 scope map[string]*TemplateCache 218 } 219 220 func NewTemplateManager() *TemplateManager { 221 return &TemplateManager{ 222 scope: make(map[string]*TemplateCache), 223 } 224 } 225 226 var ErrScopeExists = errors.New("scope already exists and cannot be added again") 227 var ErrScopeNotExists = errors.New("scope does not exist; cannot be found") 228 229 func (tm *TemplateManager) AddCache(scope, base, tmplPattern, stubPattern string) error { 230 tm.lock.Lock() 231 defer tm.lock.Unlock() 232 if _, ok := tm.scope[scope]; ok { 233 return ErrScopeExists 234 } 235 tmplConf := &TemplateConfig{ 236 StubsPattern: filepath.Join(base, stubPattern), 237 TemplatePattern: filepath.Join(base, tmplPattern), 238 } 239 tc, err := NewTemplateCacheWithSeparateStubs(tmplConf) 240 if err != nil { 241 return err 242 } 243 tm.scope[scope] = tc 244 return nil 245 } 246 247 func (tm *TemplateManager) AddCacheWithFiles(scope, base string, files ...string) error { 248 tm.lock.Lock() 249 defer tm.lock.Unlock() 250 if _, ok := tm.scope[scope]; ok { 251 return ErrScopeExists 252 } 253 var filepaths []string 254 for _, file := range files { 255 filepaths = append(filepaths, filepath.Join(base, file)) 256 } 257 tc, err := NewTemplateCacheWithFiles(nil, filepaths...) 258 if err != nil { 259 return err 260 } 261 tm.scope[scope] = tc 262 return nil 263 } 264 265 func (tm *TemplateManager) GetCache(scope string) *TemplateCache { 266 tm.lock.RLock() 267 defer tm.lock.RUnlock() 268 if tc, ok := tm.scope[scope]; ok { 269 return tc 270 } 271 return nil 272 } 273 274 func (tm *TemplateManager) Lookup(scope, name string) *template.Template { 275 tm.lock.RLock() 276 defer tm.lock.RUnlock() 277 if tc, ok := tm.scope[scope]; ok { 278 return tc.Lookup(name) 279 } 280 return nil 281 }