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  }