package cfg import ( "errors" "fmt" "strconv" "strings" "sync" "time" "github.com/wiggin77/cfg/timeconv" ) // ErrNotFound returned when an operation is attempted on a // resource that doesn't exist, such as fetching a non-existing // property name. var ErrNotFound = errors.New("not found") type sourceEntry struct { src Source props map[string]string } // Config provides methods for retrieving property values from one or more // configuration sources. type Config struct { mutexSrc sync.RWMutex mutexListeners sync.RWMutex srcs []*sourceEntry chgListeners []ChangedListener shutdown chan interface{} wantPanicOnError bool } // PrependSource inserts one or more `Sources` at the beginning of // the list of sources such that the first source will be the // source checked first when resolving a property value. func (config *Config) PrependSource(srcs ...Source) { arr := config.wrapSources(srcs...) config.mutexSrc.Lock() if config.shutdown == nil { config.shutdown = make(chan interface{}) } config.srcs = append(arr, config.srcs...) config.mutexSrc.Unlock() for _, se := range arr { if _, ok := se.src.(SourceMonitored); ok { config.monitor(se) } } } // AppendSource appends one or more `Sources` at the end of // the list of sources such that the last source will be the // source checked last when resolving a property value. func (config *Config) AppendSource(srcs ...Source) { arr := config.wrapSources(srcs...) config.mutexSrc.Lock() if config.shutdown == nil { config.shutdown = make(chan interface{}) } config.srcs = append(config.srcs, arr...) config.mutexSrc.Unlock() for _, se := range arr { if _, ok := se.src.(SourceMonitored); ok { config.monitor(se) } } } // wrapSources wraps one or more Source's and returns // them as an array of `sourceEntry`. func (config *Config) wrapSources(srcs ...Source) []*sourceEntry { arr := make([]*sourceEntry, 0, len(srcs)) for _, src := range srcs { se := &sourceEntry{src: src} config.reloadProps(se) arr = append(arr, se) } return arr } // SetWantPanicOnError sets the flag determining if Config // should panic when `GetProps` or `GetLastModified` errors // for a `Source`. func (config *Config) SetWantPanicOnError(b bool) { config.mutexSrc.Lock() config.wantPanicOnError = b config.mutexSrc.Unlock() } // ShouldPanicOnError gets the flag determining if Config // should panic when `GetProps` or `GetLastModified` errors // for a `Source`. func (config *Config) ShouldPanicOnError() (b bool) { config.mutexSrc.RLock() b = config.wantPanicOnError config.mutexSrc.RUnlock() return b } // getProp returns the value of a named property. // Each `Source` is checked, in the order created by adding via // `AppendSource` and `PrependSource`, until a value for the // property is found. func (config *Config) getProp(name string) (val string, ok bool) { config.mutexSrc.RLock() defer config.mutexSrc.RUnlock() var s string for _, se := range config.srcs { if se.props != nil { if s, ok = se.props[name]; ok { val = strings.TrimSpace(s) return } } } return } // String returns the value of the named prop as a string. // If the property is not found then the supplied default `def` // and `ErrNotFound` are returned. func (config *Config) String(name string, def string) (val string, err error) { if v, ok := config.getProp(name); ok { val = v err = nil return } err = ErrNotFound val = def return } // Int returns the value of the named prop as an `int`. // If the property is not found then the supplied default `def` // and `ErrNotFound` are returned. // // See config.String func (config *Config) Int(name string, def int) (val int, err error) { var s string if s, err = config.String(name, ""); err == nil { var i int64 if i, err = strconv.ParseInt(s, 10, 32); err == nil { val = int(i) } } if err != nil { val = def } return } // Int64 returns the value of the named prop as an `int64`. // If the property is not found then the supplied default `def` // and `ErrNotFound` are returned. // // See config.String func (config *Config) Int64(name string, def int64) (val int64, err error) { var s string if s, err = config.String(name, ""); err == nil { val, err = strconv.ParseInt(s, 10, 64) } if err != nil { val = def } return } // Float64 returns the value of the named prop as a `float64`. // If the property is not found then the supplied default `def` // and `ErrNotFound` are returned. // // See config.String func (config *Config) Float64(name string, def float64) (val float64, err error) { var s string if s, err = config.String(name, ""); err == nil { val, err = strconv.ParseFloat(s, 64) } if err != nil { val = def } return } // Bool returns the value of the named prop as a `bool`. // If the property is not found then the supplied default `def` // and `ErrNotFound` are returned. // // Supports (t, true, 1, y, yes) for true, and (f, false, 0, n, no) for false, // all case-insensitive. // // See config.String func (config *Config) Bool(name string, def bool) (val bool, err error) { var s string if s, err = config.String(name, ""); err == nil { switch strings.ToLower(s) { case "t", "true", "1", "y", "yes": val = true case "f", "false", "0", "n", "no": val = false default: err = errors.New("invalid syntax") } } if err != nil { val = def } return } // Duration returns the value of the named prop as a `time.Duration`, representing // a span of time. // // Units of measure are supported: ms, sec, min, hour, day, week, year. // See config.UnitsToMillis for a complete list of units supported. // // If the property is not found then the supplied default `def` // and `ErrNotFound` are returned. // // See config.String func (config *Config) Duration(name string, def time.Duration) (val time.Duration, err error) { var s string if s, err = config.String(name, ""); err == nil { var ms int64 ms, err = timeconv.ParseMilliseconds(s) val = time.Duration(ms) * time.Millisecond } if err != nil { val = def } return } // AddChangedListener adds a listener that will receive notifications // whenever one or more property values change within the config. func (config *Config) AddChangedListener(l ChangedListener) { config.mutexListeners.Lock() defer config.mutexListeners.Unlock() config.chgListeners = append(config.chgListeners, l) } // RemoveChangedListener removes all instances of a ChangedListener. // Returns `ErrNotFound` if the listener was not present. func (config *Config) RemoveChangedListener(l ChangedListener) error { config.mutexListeners.Lock() defer config.mutexListeners.Unlock() dest := make([]ChangedListener, 0, len(config.chgListeners)) err := ErrNotFound // Remove all instances of the listener by // copying list while filtering. for _, s := range config.chgListeners { if s != l { dest = append(dest, s) } else { err = nil } } config.chgListeners = dest return err } // Shutdown can be called to stop monitoring of all config sources. func (config *Config) Shutdown() { config.mutexSrc.RLock() defer config.mutexSrc.RUnlock() if config.shutdown != nil { close(config.shutdown) } } // onSourceChanged is called whenever one or more properties of a // config source has changed. func (config *Config) onSourceChanged(src SourceMonitored) { defer func() { if p := recover(); p != nil { fmt.Println(p) } }() config.mutexListeners.RLock() defer config.mutexListeners.RUnlock() for _, l := range config.chgListeners { l.ConfigChanged(config, src) } } // monitor periodically checks a config source for changes. func (config *Config) monitor(se *sourceEntry) { go func(se *sourceEntry, shutdown <-chan interface{}) { var src SourceMonitored var ok bool if src, ok = se.src.(SourceMonitored); !ok { return } paused := false last := time.Time{} freq := src.GetMonitorFreq() if freq <= 0 { paused = true freq = 10 last, _ = src.GetLastModified() } timer := time.NewTimer(freq) for { select { case <-timer.C: if !paused { if latest, err := src.GetLastModified(); err != nil { if config.ShouldPanicOnError() { panic(fmt.Sprintf("error <%v> getting last modified for %v", err, src)) } } else { if last.Before(latest) { last = latest config.reloadProps(se) // TODO: calc diff and provide detailed changes config.onSourceChanged(src) } } } freq = src.GetMonitorFreq() if freq <= 0 { paused = true freq = 10 } else { paused = false } timer.Reset(freq) case <-shutdown: // stop the timer and exit if !timer.Stop() { <-timer.C } return } } }(se, config.shutdown) } // reloadProps causes a Source to reload its properties. func (config *Config) reloadProps(se *sourceEntry) { config.mutexSrc.Lock() defer config.mutexSrc.Unlock() m, err := se.src.GetProps() if err != nil { if config.wantPanicOnError { panic(fmt.Sprintf("GetProps error for %v", se.src)) } return } se.props = make(map[string]string) for k, v := range m { se.props[k] = v } }