|
1 // Copyright 2018 Frank Schroeder. All rights reserved. |
|
2 // Use of this source code is governed by a BSD-style |
|
3 // license that can be found in the LICENSE file. |
|
4 |
|
5 package properties |
|
6 |
|
7 import ( |
|
8 "fmt" |
|
9 "io/ioutil" |
|
10 "net/http" |
|
11 "os" |
|
12 "strings" |
|
13 ) |
|
14 |
|
15 // Encoding specifies encoding of the input data. |
|
16 type Encoding uint |
|
17 |
|
18 const ( |
|
19 // utf8Default is a private placeholder for the zero value of Encoding to |
|
20 // ensure that it has the correct meaning. UTF8 is the default encoding but |
|
21 // was assigned a non-zero value which cannot be changed without breaking |
|
22 // existing code. Clients should continue to use the public constants. |
|
23 utf8Default Encoding = iota |
|
24 |
|
25 // UTF8 interprets the input data as UTF-8. |
|
26 UTF8 |
|
27 |
|
28 // ISO_8859_1 interprets the input data as ISO-8859-1. |
|
29 ISO_8859_1 |
|
30 ) |
|
31 |
|
32 type Loader struct { |
|
33 // Encoding determines how the data from files and byte buffers |
|
34 // is interpreted. For URLs the Content-Type header is used |
|
35 // to determine the encoding of the data. |
|
36 Encoding Encoding |
|
37 |
|
38 // DisableExpansion configures the property expansion of the |
|
39 // returned property object. When set to true, the property values |
|
40 // will not be expanded and the Property object will not be checked |
|
41 // for invalid expansion expressions. |
|
42 DisableExpansion bool |
|
43 |
|
44 // IgnoreMissing configures whether missing files or URLs which return |
|
45 // 404 are reported as errors. When set to true, missing files and 404 |
|
46 // status codes are not reported as errors. |
|
47 IgnoreMissing bool |
|
48 } |
|
49 |
|
50 // Load reads a buffer into a Properties struct. |
|
51 func (l *Loader) LoadBytes(buf []byte) (*Properties, error) { |
|
52 return l.loadBytes(buf, l.Encoding) |
|
53 } |
|
54 |
|
55 // LoadAll reads the content of multiple URLs or files in the given order into |
|
56 // a Properties struct. If IgnoreMissing is true then a 404 status code or |
|
57 // missing file will not be reported as error. Encoding sets the encoding for |
|
58 // files. For the URLs see LoadURL for the Content-Type header and the |
|
59 // encoding. |
|
60 func (l *Loader) LoadAll(names []string) (*Properties, error) { |
|
61 all := NewProperties() |
|
62 for _, name := range names { |
|
63 n, err := expandName(name) |
|
64 if err != nil { |
|
65 return nil, err |
|
66 } |
|
67 |
|
68 var p *Properties |
|
69 switch { |
|
70 case strings.HasPrefix(n, "http://"): |
|
71 p, err = l.LoadURL(n) |
|
72 case strings.HasPrefix(n, "https://"): |
|
73 p, err = l.LoadURL(n) |
|
74 default: |
|
75 p, err = l.LoadFile(n) |
|
76 } |
|
77 if err != nil { |
|
78 return nil, err |
|
79 } |
|
80 all.Merge(p) |
|
81 } |
|
82 |
|
83 all.DisableExpansion = l.DisableExpansion |
|
84 if all.DisableExpansion { |
|
85 return all, nil |
|
86 } |
|
87 return all, all.check() |
|
88 } |
|
89 |
|
90 // LoadFile reads a file into a Properties struct. |
|
91 // If IgnoreMissing is true then a missing file will not be |
|
92 // reported as error. |
|
93 func (l *Loader) LoadFile(filename string) (*Properties, error) { |
|
94 data, err := ioutil.ReadFile(filename) |
|
95 if err != nil { |
|
96 if l.IgnoreMissing && os.IsNotExist(err) { |
|
97 LogPrintf("properties: %s not found. skipping", filename) |
|
98 return NewProperties(), nil |
|
99 } |
|
100 return nil, err |
|
101 } |
|
102 return l.loadBytes(data, l.Encoding) |
|
103 } |
|
104 |
|
105 // LoadURL reads the content of the URL into a Properties struct. |
|
106 // |
|
107 // The encoding is determined via the Content-Type header which |
|
108 // should be set to 'text/plain'. If the 'charset' parameter is |
|
109 // missing, 'iso-8859-1' or 'latin1' the encoding is set to |
|
110 // ISO-8859-1. If the 'charset' parameter is set to 'utf-8' the |
|
111 // encoding is set to UTF-8. A missing content type header is |
|
112 // interpreted as 'text/plain; charset=utf-8'. |
|
113 func (l *Loader) LoadURL(url string) (*Properties, error) { |
|
114 resp, err := http.Get(url) |
|
115 if err != nil { |
|
116 return nil, fmt.Errorf("properties: error fetching %q. %s", url, err) |
|
117 } |
|
118 |
|
119 if resp.StatusCode == 404 && l.IgnoreMissing { |
|
120 LogPrintf("properties: %s returned %d. skipping", url, resp.StatusCode) |
|
121 return NewProperties(), nil |
|
122 } |
|
123 |
|
124 if resp.StatusCode != 200 { |
|
125 return nil, fmt.Errorf("properties: %s returned %d", url, resp.StatusCode) |
|
126 } |
|
127 |
|
128 body, err := ioutil.ReadAll(resp.Body) |
|
129 if err != nil { |
|
130 return nil, fmt.Errorf("properties: %s error reading response. %s", url, err) |
|
131 } |
|
132 defer resp.Body.Close() |
|
133 |
|
134 ct := resp.Header.Get("Content-Type") |
|
135 var enc Encoding |
|
136 switch strings.ToLower(ct) { |
|
137 case "text/plain", "text/plain; charset=iso-8859-1", "text/plain; charset=latin1": |
|
138 enc = ISO_8859_1 |
|
139 case "", "text/plain; charset=utf-8": |
|
140 enc = UTF8 |
|
141 default: |
|
142 return nil, fmt.Errorf("properties: invalid content type %s", ct) |
|
143 } |
|
144 |
|
145 return l.loadBytes(body, enc) |
|
146 } |
|
147 |
|
148 func (l *Loader) loadBytes(buf []byte, enc Encoding) (*Properties, error) { |
|
149 p, err := parse(convert(buf, enc)) |
|
150 if err != nil { |
|
151 return nil, err |
|
152 } |
|
153 p.DisableExpansion = l.DisableExpansion |
|
154 if p.DisableExpansion { |
|
155 return p, nil |
|
156 } |
|
157 return p, p.check() |
|
158 } |
|
159 |
|
160 // Load reads a buffer into a Properties struct. |
|
161 func Load(buf []byte, enc Encoding) (*Properties, error) { |
|
162 l := &Loader{Encoding: enc} |
|
163 return l.LoadBytes(buf) |
|
164 } |
|
165 |
|
166 // LoadString reads an UTF8 string into a properties struct. |
|
167 func LoadString(s string) (*Properties, error) { |
|
168 l := &Loader{Encoding: UTF8} |
|
169 return l.LoadBytes([]byte(s)) |
|
170 } |
|
171 |
|
172 // LoadMap creates a new Properties struct from a string map. |
|
173 func LoadMap(m map[string]string) *Properties { |
|
174 p := NewProperties() |
|
175 for k, v := range m { |
|
176 p.Set(k, v) |
|
177 } |
|
178 return p |
|
179 } |
|
180 |
|
181 // LoadFile reads a file into a Properties struct. |
|
182 func LoadFile(filename string, enc Encoding) (*Properties, error) { |
|
183 l := &Loader{Encoding: enc} |
|
184 return l.LoadAll([]string{filename}) |
|
185 } |
|
186 |
|
187 // LoadFiles reads multiple files in the given order into |
|
188 // a Properties struct. If 'ignoreMissing' is true then |
|
189 // non-existent files will not be reported as error. |
|
190 func LoadFiles(filenames []string, enc Encoding, ignoreMissing bool) (*Properties, error) { |
|
191 l := &Loader{Encoding: enc, IgnoreMissing: ignoreMissing} |
|
192 return l.LoadAll(filenames) |
|
193 } |
|
194 |
|
195 // LoadURL reads the content of the URL into a Properties struct. |
|
196 // See Loader#LoadURL for details. |
|
197 func LoadURL(url string) (*Properties, error) { |
|
198 l := &Loader{Encoding: UTF8} |
|
199 return l.LoadAll([]string{url}) |
|
200 } |
|
201 |
|
202 // LoadURLs reads the content of multiple URLs in the given order into a |
|
203 // Properties struct. If IgnoreMissing is true then a 404 status code will |
|
204 // not be reported as error. See Loader#LoadURL for the Content-Type header |
|
205 // and the encoding. |
|
206 func LoadURLs(urls []string, ignoreMissing bool) (*Properties, error) { |
|
207 l := &Loader{Encoding: UTF8, IgnoreMissing: ignoreMissing} |
|
208 return l.LoadAll(urls) |
|
209 } |
|
210 |
|
211 // LoadAll reads the content of multiple URLs or files in the given order into a |
|
212 // Properties struct. If 'ignoreMissing' is true then a 404 status code or missing file will |
|
213 // not be reported as error. Encoding sets the encoding for files. For the URLs please see |
|
214 // LoadURL for the Content-Type header and the encoding. |
|
215 func LoadAll(names []string, enc Encoding, ignoreMissing bool) (*Properties, error) { |
|
216 l := &Loader{Encoding: enc, IgnoreMissing: ignoreMissing} |
|
217 return l.LoadAll(names) |
|
218 } |
|
219 |
|
220 // MustLoadString reads an UTF8 string into a Properties struct and |
|
221 // panics on error. |
|
222 func MustLoadString(s string) *Properties { |
|
223 return must(LoadString(s)) |
|
224 } |
|
225 |
|
226 // MustLoadFile reads a file into a Properties struct and |
|
227 // panics on error. |
|
228 func MustLoadFile(filename string, enc Encoding) *Properties { |
|
229 return must(LoadFile(filename, enc)) |
|
230 } |
|
231 |
|
232 // MustLoadFiles reads multiple files in the given order into |
|
233 // a Properties struct and panics on error. If 'ignoreMissing' |
|
234 // is true then non-existent files will not be reported as error. |
|
235 func MustLoadFiles(filenames []string, enc Encoding, ignoreMissing bool) *Properties { |
|
236 return must(LoadFiles(filenames, enc, ignoreMissing)) |
|
237 } |
|
238 |
|
239 // MustLoadURL reads the content of a URL into a Properties struct and |
|
240 // panics on error. |
|
241 func MustLoadURL(url string) *Properties { |
|
242 return must(LoadURL(url)) |
|
243 } |
|
244 |
|
245 // MustLoadURLs reads the content of multiple URLs in the given order into a |
|
246 // Properties struct and panics on error. If 'ignoreMissing' is true then a 404 |
|
247 // status code will not be reported as error. |
|
248 func MustLoadURLs(urls []string, ignoreMissing bool) *Properties { |
|
249 return must(LoadURLs(urls, ignoreMissing)) |
|
250 } |
|
251 |
|
252 // MustLoadAll reads the content of multiple URLs or files in the given order into a |
|
253 // Properties struct. If 'ignoreMissing' is true then a 404 status code or missing file will |
|
254 // not be reported as error. Encoding sets the encoding for files. For the URLs please see |
|
255 // LoadURL for the Content-Type header and the encoding. It panics on error. |
|
256 func MustLoadAll(names []string, enc Encoding, ignoreMissing bool) *Properties { |
|
257 return must(LoadAll(names, enc, ignoreMissing)) |
|
258 } |
|
259 |
|
260 func must(p *Properties, err error) *Properties { |
|
261 if err != nil { |
|
262 ErrorHandler(err) |
|
263 } |
|
264 return p |
|
265 } |
|
266 |
|
267 // expandName expands ${ENV_VAR} expressions in a name. |
|
268 // If the environment variable does not exist then it will be replaced |
|
269 // with an empty string. Malformed expressions like "${ENV_VAR" will |
|
270 // be reported as error. |
|
271 func expandName(name string) (string, error) { |
|
272 return expand(name, []string{}, "${", "}", make(map[string]string)) |
|
273 } |
|
274 |
|
275 // Interprets a byte buffer either as an ISO-8859-1 or UTF-8 encoded string. |
|
276 // For ISO-8859-1 we can convert each byte straight into a rune since the |
|
277 // first 256 unicode code points cover ISO-8859-1. |
|
278 func convert(buf []byte, enc Encoding) string { |
|
279 switch enc { |
|
280 case utf8Default, UTF8: |
|
281 return string(buf) |
|
282 case ISO_8859_1: |
|
283 runes := make([]rune, len(buf)) |
|
284 for i, b := range buf { |
|
285 runes[i] = rune(b) |
|
286 } |
|
287 return string(runes) |
|
288 default: |
|
289 ErrorHandler(fmt.Errorf("unsupported encoding %v", enc)) |
|
290 } |
|
291 panic("ErrorHandler should exit") |
|
292 } |