summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/wiggin77/cfg/config.go
blob: 0e958102e7d6c47eda4b996ed9279aa4f2b5443f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
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
	}
}